diff --git a/README.md b/README.md index f20e05ea6..97317a302 100644 --- a/README.md +++ b/README.md @@ -208,11 +208,11 @@ The following sets of tools are available (toolsets marked with ✓ in the Defau -| Toolset | Description | Default | -|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| -| config | View and manage the current local Kubernetes configuration (kubeconfig) | ✓ | -| core | Most common tools for Kubernetes management (Pods, Generic Resources, Events, etc.) | ✓ | -| helm | Tools for managing Helm charts and releases | ✓ | +| Toolset | Description | Default | +|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| +| config | View and manage the current local Kubernetes configuration (kubeconfig) | ✓ | +| core | Most common tools for Kubernetes management (Pods, Generic Resources, Events, etc.) | ✓ | +| helm | Tools for managing Helm charts and releases | ✓ | | kiali | Most common tools for managing Kiali, check the [Kiali documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/KIALI.md) for more details. | | diff --git a/pkg/kiali/kiali.go b/pkg/kiali/kiali.go index 5d777e8a5..2755ddca2 100644 --- a/pkg/kiali/kiali.go +++ b/pkg/kiali/kiali.go @@ -60,12 +60,19 @@ func (k *Kiali) validateAndGetURL(endpoint string) (string, error) { if err != nil { return "", fmt.Errorf("invalid endpoint path: %w", err) } + // Reject absolute URLs - endpoint should be a relative path + if endpointURL.Scheme != "" || endpointURL.Host != "" { + return "", fmt.Errorf("endpoint must be a relative path, not an absolute URL") + } resultURL, err := url.JoinPath(baseURL.String(), endpointURL.Path) if err != nil { return "", fmt.Errorf("failed to join kiali base URL with endpoint path: %w", err) } - u, _ := url.Parse(resultURL) + u, err := url.Parse(resultURL) + if err != nil { + return "", fmt.Errorf("failed to parse joined URL: %w", err) + } u.RawQuery = endpointURL.RawQuery u.Fragment = endpointURL.Fragment @@ -145,7 +152,10 @@ func (k *Kiali) executeRequest(ctx context.Context, method, endpoint, contentTyp return "", err } defer func() { _ = resp.Body.Close() }() - respBody, _ := io.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response body: %w", err) + } if resp.StatusCode < 200 || resp.StatusCode >= 300 { if len(respBody) > 0 { return "", fmt.Errorf("kiali API error: %s", strings.TrimSpace(string(respBody))) diff --git a/pkg/kiali/kiali_test.go b/pkg/kiali/kiali_test.go index 5028c32b7..403bfd2d9 100644 --- a/pkg/kiali/kiali_test.go +++ b/pkg/kiali/kiali_test.go @@ -128,6 +128,49 @@ func (s *KialiSuite) TestValidateAndGetURL() { s.Equal("true", u.Query().Get("health"), "Unexpected query parameter health") }) }) + + s.Run("Rejects absolute URLs in endpoint", func() { + s.Config = test.Must(config.ReadToml([]byte(` + [toolset_configs.kiali] + url = "https://kiali.example/" + insecure = true + `))) + k := NewKiali(s.Config, s.MockServer.Config()) + + s.Run("rejects http URLs", func() { + _, err := k.validateAndGetURL("http://other-server.com/api") + s.Require().Error(err, "Expected error for absolute URL") + s.ErrorContains(err, "endpoint must be a relative path", "Unexpected error message") + }) + + s.Run("rejects https URLs", func() { + _, err := k.validateAndGetURL("https://other-server.com/api") + s.Require().Error(err, "Expected error for absolute URL") + s.ErrorContains(err, "endpoint must be a relative path", "Unexpected error message") + }) + + s.Run("rejects URLs with host but no scheme", func() { + _, err := k.validateAndGetURL("//other-server.com/api") + s.Require().Error(err, "Expected error for URL with host") + s.ErrorContains(err, "endpoint must be a relative path", "Unexpected error message") + }) + }) + + s.Run("Preserves fragment in endpoint", func() { + s.Config = test.Must(config.ReadToml([]byte(` + [toolset_configs.kiali] + url = "https://kiali.example/" + insecure = true + `))) + k := NewKiali(s.Config, s.MockServer.Config()) + + full, err := k.validateAndGetURL("/api/path#section") + s.Require().NoError(err, "Expected no error validating URL with fragment") + u, err := url.Parse(full) + s.Require().NoError(err, "Expected to parse full URL") + s.Equal("/api/path", u.Path, "Unexpected path in parsed URL") + s.Equal("section", u.Fragment, "Unexpected fragment in parsed URL") + }) } // CurrentAuthorizationHeader behavior is now implicit via executeRequest using Manager.BearerToken diff --git a/pkg/toolsets/kiali/toolset.go b/pkg/toolsets/kiali/toolset.go index c89afcd49..7467992cf 100644 --- a/pkg/toolsets/kiali/toolset.go +++ b/pkg/toolsets/kiali/toolset.go @@ -17,7 +17,7 @@ func (t *Toolset) GetName() string { } func (t *Toolset) GetDescription() string { - return "Most common tools for managing Kiali, check the [Kiali integration documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/KIALI_INTEGRATION.md) for more details." + return "Most common tools for managing Kiali, check the [Kiali documentation](https://github.com/containers/kubernetes-mcp-server/blob/main/docs/KIALI.md) for more details." } func (t *Toolset) GetTools(_ internalk8s.Openshift) []api.ServerTool {