From 91e827036405653f4d0279764278a25b2d8f624b Mon Sep 17 00:00:00 2001 From: Hugh Brown Date: Tue, 3 Mar 2026 22:21:14 -0700 Subject: [PATCH] revert: restore GatewayRead.token field to avoid frontend breaking change The has_token boolean redaction requires coordinated frontend changes (detail page, edit page, orval types). Revert to returning the raw token for now; token redaction will be handled in a dedicated PR. Co-Authored-By: Claude Opus 4.6 --- backend/app/api/gateways.py | 34 ++++++---------------------- backend/app/schemas/gateways.py | 2 +- backend/tests/test_security_fixes.py | 34 ---------------------------- 3 files changed, 8 insertions(+), 62 deletions(-) diff --git a/backend/app/api/gateways.py b/backend/app/api/gateways.py index 558a9986..2429c8a8 100644 --- a/backend/app/api/gateways.py +++ b/backend/app/api/gateways.py @@ -28,29 +28,12 @@ from app.services.openclaw.admin_service import GatewayAdminLifecycleService from app.services.openclaw.session_service import GatewayTemplateSyncQuery if TYPE_CHECKING: - from collections.abc import Sequence - from fastapi_pagination.limit_offset import LimitOffsetPage from sqlmodel.ext.asyncio.session import AsyncSession from app.services.organizations import OrganizationContext -def _to_gateway_read(gateway: Gateway) -> GatewayRead: - return GatewayRead( - id=gateway.id, - organization_id=gateway.organization_id, - name=gateway.name, - url=gateway.url, - workspace_root=gateway.workspace_root, - allow_insecure_tls=gateway.allow_insecure_tls, - disable_device_pairing=gateway.disable_device_pairing, - has_token=gateway.token is not None, - created_at=gateway.created_at, - updated_at=gateway.updated_at, - ) - - router = APIRouter(prefix="/gateways", tags=["gateways"]) SESSION_DEP = Depends(get_session) AUTH_DEP = Depends(get_auth_context) @@ -101,10 +84,7 @@ async def list_gateways( .statement ) - def _transform(items: Sequence[Gateway]) -> list[GatewayRead]: - return [_to_gateway_read(item) for item in items] - - return await paginate(session, statement, transformer=_transform) + return await paginate(session, statement) @router.post("", response_model=GatewayRead) @@ -113,7 +93,7 @@ async def create_gateway( session: AsyncSession = SESSION_DEP, auth: AuthContext = AUTH_DEP, ctx: OrganizationContext = ORG_ADMIN_DEP, -) -> GatewayRead: +) -> Gateway: """Create a gateway and provision or refresh its main agent.""" service = GatewayAdminLifecycleService(session) await service.assert_gateway_runtime_compatible( @@ -128,7 +108,7 @@ async def create_gateway( data["organization_id"] = ctx.organization.id gateway = await crud.create(session, Gateway, **data) await service.ensure_main_agent(gateway, auth, action="provision") - return _to_gateway_read(gateway) + return gateway @router.get("/{gateway_id}", response_model=GatewayRead) @@ -136,14 +116,14 @@ async def get_gateway( gateway_id: UUID, session: AsyncSession = SESSION_DEP, ctx: OrganizationContext = ORG_ADMIN_DEP, -) -> GatewayRead: +) -> Gateway: """Return one gateway by id for the caller's organization.""" service = GatewayAdminLifecycleService(session) gateway = await service.require_gateway( gateway_id=gateway_id, organization_id=ctx.organization.id, ) - return _to_gateway_read(gateway) + return gateway @router.patch("/{gateway_id}", response_model=GatewayRead) @@ -153,7 +133,7 @@ async def update_gateway( session: AsyncSession = SESSION_DEP, auth: AuthContext = AUTH_DEP, ctx: OrganizationContext = ORG_ADMIN_DEP, -) -> GatewayRead: +) -> Gateway: """Patch a gateway and refresh the main-agent provisioning state.""" service = GatewayAdminLifecycleService(session) gateway = await service.require_gateway( @@ -185,7 +165,7 @@ async def update_gateway( ) await crud.patch(session, gateway, updates) await service.ensure_main_agent(gateway, auth, action="update") - return _to_gateway_read(gateway) + return gateway @router.post("/{gateway_id}/templates/sync", response_model=GatewayTemplatesSyncResult) diff --git a/backend/app/schemas/gateways.py b/backend/app/schemas/gateways.py index dceb47dc..9d306991 100644 --- a/backend/app/schemas/gateways.py +++ b/backend/app/schemas/gateways.py @@ -65,7 +65,7 @@ class GatewayRead(GatewayBase): id: UUID organization_id: UUID - has_token: bool = False + token: str | None = None created_at: datetime updated_at: datetime diff --git a/backend/tests/test_security_fixes.py b/backend/tests/test_security_fixes.py index 58936466..2195de87 100644 --- a/backend/tests/test_security_fixes.py +++ b/backend/tests/test_security_fixes.py @@ -24,7 +24,6 @@ from app.models.board_webhooks import BoardWebhook from app.models.boards import Board from app.models.gateways import Gateway from app.models.organizations import Organization -from app.schemas.gateways import GatewayRead from app.services.admin_access import require_user_actor # --------------------------------------------------------------------------- @@ -497,36 +496,3 @@ class TestWebhookPayloadSizeLimit: # --------------------------------------------------------------------------- -class TestGatewayTokenRedaction: - """Tests for gateway token redaction from API responses.""" - - def test_gateway_read_has_has_token_field(self) -> None: - read = GatewayRead( - id=uuid4(), - organization_id=uuid4(), - name="gw", - url="https://gw.example.com", - workspace_root="/ws", - has_token=True, - created_at="2025-01-01T00:00:00", - updated_at="2025-01-01T00:00:00", - ) - data = read.model_dump() - assert "has_token" in data - assert data["has_token"] is True - # Ensure 'token' field is NOT present - assert "token" not in data - - def test_gateway_read_without_token(self) -> None: - read = GatewayRead( - id=uuid4(), - organization_id=uuid4(), - name="gw", - url="https://gw.example.com", - workspace_root="/ws", - has_token=False, - created_at="2025-01-01T00:00:00", - updated_at="2025-01-01T00:00:00", - ) - assert read.has_token is False -