cli: authenticate Hub HTTP requests with License-Key (gated-auth phase 1)

Add a License-Key RoundTripper plus NewHubHTTPClient / HubAuthTransport
helpers in utils, and route the MCP runner's Hub clients (connect, --url
validate, file download, tools list) through them so the CLI keeps working
once the Hub enforces auth. License-Key is a custom header, so it survives
the kube API-server service proxy (a bearer token would not). Surface
ErrHubAuthRequired on a 401/302 from the Hub.

console already sends License-Key on its ws connection. pprof is not
covered here: it shells out to 'go tool pprof <hubUrl>' / opens a browser,
external fetches that can't carry a custom header (follow-up).
This commit is contained in:
Volodymyr Stoiko
2026-06-15 20:59:27 +00:00
parent 9396e64b9b
commit 6ba67ef3f5
2 changed files with 60 additions and 5 deletions

View File

@@ -2,17 +2,67 @@ package utils
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
)
const (
X_KUBESHARK_CAPTURE_HEADER_KEY = "X-Kubeshark-Capture"
X_KUBESHARK_CAPTURE_HEADER_IGNORE_VALUE = "ignore"
LICENSE_KEY_HEADER = "License-Key"
)
// ErrHubAuthRequired indicates the Hub rejected the request because auth is
// enabled but no valid credential was presented (missing/expired license).
var ErrHubAuthRequired = errors.New("hub requires authentication: set a valid license (config 'license') or credentials")
// licenseKeyRoundTripper attaches the License-Key header to every request so
// any client built with it authenticates to a gated Hub. License-Key is a
// custom (non-Authorization) header so it survives the kube API-server
// service proxy, unlike a bearer token.
type licenseKeyRoundTripper struct {
licenseKey string
base http.RoundTripper
}
func (rt *licenseKeyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
base := rt.base
if base == nil {
base = http.DefaultTransport
}
if rt.licenseKey != "" && req.Header.Get(LICENSE_KEY_HEADER) == "" {
req = req.Clone(req.Context())
req.Header.Set(LICENSE_KEY_HEADER, rt.licenseKey)
}
return base.RoundTrip(req)
}
// NewHubHTTPClient returns an *http.Client that authenticates to the Hub by
// attaching the License-Key header to every request.
func NewHubHTTPClient(timeout time.Duration, licenseKey string) *http.Client {
return &http.Client{
Timeout: timeout,
Transport: &licenseKeyRoundTripper{licenseKey: licenseKey},
}
}
// HubAuthTransport wraps base so requests carry the License-Key header. Use
// when a client needs custom transport settings (e.g. streaming downloads)
// but must still authenticate to the Hub.
func HubAuthTransport(licenseKey string, base http.RoundTripper) http.RoundTripper {
return &licenseKeyRoundTripper{licenseKey: licenseKey, base: base}
}
// IsAuthRequired reports whether the response indicates the Hub demanded
// authentication — a 401, or a 302 redirect to an SSO login page.
func IsAuthRequired(resp *http.Response) bool {
return resp != nil && (resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusFound)
}
// Get - When err is nil, resp always contains a non-nil resp.Body.
// Caller should close resp.Body when done reading from it.
func Get(url string, client *http.Client) (*http.Response, error) {