Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
28 changes: 28 additions & 0 deletions pkg/custom_tool/handler/custom_tool_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type CustomToolHandler interface {
Update(c *gin.Context)
Delete(c *gin.Context)
Test(c *gin.Context)
TestPayload(c *gin.Context)
}

// customToolHandler implements the CustomToolHandler interface.
Expand Down Expand Up @@ -75,6 +76,10 @@ func (h *customToolHandler) RegisterRoutesMiddleware(router gin.IRouter) {
permissionMiddleware.RequirePermission("ai_custom_tools", "delete"),
h.Delete)

// EVO-1738: stateless test-before-save (validates the payload typed in the wizard).
customTools.POST("/test",
permissionMiddleware.RequirePermission("ai_custom_tools", "read"),
h.TestPayload)
// Test permissions
customTools.GET("/:id/test",
permissionMiddleware.RequirePermission("ai_custom_tools", "read"),
Expand Down Expand Up @@ -323,3 +328,26 @@ func (h *customToolHandler) Test(c *gin.Context) {

response.SuccessResponse(c, customTool, "Custom tool test completed successfully", http.StatusOK)
}

// TestPayload tests an UNSAVED tool payload (test-before-save). EVO-1738.
func (h *customToolHandler) TestPayload(c *gin.Context) {
var req struct {
Method string `json:"method" binding:"required"`
Endpoint string `json:"endpoint" binding:"required"`
Headers map[string]string `json:"headers"`
BodyParams map[string]interface{} `json:"body_params"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.ValidationErrorResponse(c, err)
return
}

testResult, err := h.customToolService.TestPayload(c.Request.Context(), req.Method, req.Endpoint, req.Headers, req.BodyParams)
if err != nil {
code, message, httpCode := errors.HandleError(err)
response.ErrorResponse(c, code, message, nil, httpCode)
return
}

response.SuccessResponse(c, gin.H{"test_result": testResult}, "Custom tool test completed successfully", http.StatusOK)
}
17 changes: 17 additions & 0 deletions pkg/custom_tool/service/custom_tool_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,9 @@ type CustomToolService interface {
Delete(ctx context.Context, id uuid.UUID) (bool, error)
ConvertToHTTPTool(tool model.CustomToolResponse) map[string]interface{}
Test(ctx context.Context, id uuid.UUID) (*model.CustomToolTestResponse, error)
// EVO-1738: stateless test of an UNSAVED tool payload (test-before-save in the
// wizard). Same SSRF-hardened runToolTest as Test, without requiring a saved tool.
TestPayload(ctx context.Context, method, endpoint string, headers map[string]string, bodyParams map[string]interface{}) (*model.TestResult, error)
}

type customToolService struct {
Expand Down Expand Up @@ -438,3 +441,17 @@ func (s *customToolService) Test(ctx context.Context, id uuid.UUID) (*model.Cust
TestResult: testResult,
}, nil
}

// TestPayload runs the SSRF-hardened tool request against an UNSAVED payload —
// powers the wizard's "test before save" (EVO-1738).
func (s *customToolService) TestPayload(ctx context.Context, method, endpoint string, headers map[string]string, bodyParams map[string]interface{}) (*model.TestResult, error) {
method = strings.ToUpper(method)
switch method {
case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch, http.MethodHead, http.MethodOptions:
// supported
default:
return nil, fmt.Errorf("unsupported method: %s", method)
}

return runToolTest(ctx, method, endpoint, headers, bodyParams), nil
}
24 changes: 24 additions & 0 deletions pkg/custom_tool/service/custom_tool_test_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -466,3 +466,27 @@ func testIsPublicIPBody(t *testing.T) {
}
}
}

// EVO-1738: the public TestPayload wrapper (test-before-save) runs the same request
// as Test against an UNSAVED payload, and fails fast on an unsupported method.
func TestTestPayload_UnsavedTool_RunsAndValidatesMethod(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"ok":true}`))
}))
defer srv.Close()

svc := &customToolService{}
res, err := svc.TestPayload(context.Background(), "get", srv.URL, nil, nil)
if err != nil {
t.Fatalf("TestPayload: %v", err)
}
if !res.Success || res.StatusCode != http.StatusOK {
t.Fatalf("want success 200, got success=%v code=%d err=%q", res.Success, res.StatusCode, res.Error)
}

if _, err := svc.TestPayload(context.Background(), "TRACE", srv.URL, nil, nil); err == nil {
t.Fatal("expected error for unsupported method")
}
}
Loading