mirror of https://github.com/helm/helm
Merge 2f65e55e8c into 53dfa521e0
commit
a64eca9e68
@ -0,0 +1,58 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package kubeenv
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SessionHeader is the HTTP header Helm sends on Kubernetes API requests so
|
||||
// requests from a single command execution can be correlated for auditing.
|
||||
const SessionHeader = "helm-session"
|
||||
|
||||
// SessionRoundTripper sets the SessionHeader header carrying SessionID on
|
||||
// every request sent through the wrapped [http.RoundTripper].
|
||||
type SessionRoundTripper struct {
|
||||
Wrapped http.RoundTripper
|
||||
SessionID string
|
||||
}
|
||||
|
||||
// RoundTrip implements [http.RoundTripper].
|
||||
func (rt *SessionRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Clone the request: a RoundTripper must not mutate the request it is given.
|
||||
r := req.Clone(req.Context())
|
||||
r.Header.Set(SessionHeader, rt.SessionID)
|
||||
return rt.Wrapped.RoundTrip(r)
|
||||
}
|
||||
|
||||
// NewSessionID generates a fresh 128-bit session identifier. Callers should
|
||||
// generate one ID per logical operation: the Helm CLI creates a single
|
||||
// [cli.EnvSettings] per command invocation, so every Kubernetes client built
|
||||
// while running one command shares a session, while separate settings (e.g.
|
||||
// SDK clients performing distinct operations) get distinct sessions.
|
||||
func NewSessionID() string {
|
||||
var b [16]byte
|
||||
if _, err := rand.Read(b[:]); err == nil {
|
||||
return hex.EncodeToString(b[:])
|
||||
}
|
||||
// crypto/rand effectively never fails; fall back to a timestamp-based value.
|
||||
return "fallback-" + strconv.FormatInt(time.Now().UnixNano(), 16)
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package kubeenv
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// captureRoundTripper records the request it receives and returns a canned response.
|
||||
type captureRoundTripper struct {
|
||||
got *http.Request
|
||||
}
|
||||
|
||||
func (c *captureRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
c.got = req
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: http.Header{}}, nil
|
||||
}
|
||||
|
||||
func TestSessionRoundTripper(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sessionID string
|
||||
}{
|
||||
{name: "sets the session header", sessionID: "test-session-id"},
|
||||
{name: "sets the generated session ID", sessionID: NewSessionID()},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
capture := &captureRoundTripper{}
|
||||
rt := &SessionRoundTripper{Wrapped: capture, SessionID: tt.sessionID}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://example.com/api", nil)
|
||||
require.NoError(t, err)
|
||||
// A pre-existing header with the same name must be replaced, not duplicated.
|
||||
req.Header.Set(SessionHeader, "stale")
|
||||
|
||||
_, err = rt.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, capture.got)
|
||||
|
||||
assert.Equal(t, tt.sessionID, capture.got.Header.Get(SessionHeader))
|
||||
assert.Len(t, capture.got.Header.Values(SessionHeader), 1)
|
||||
// The caller's request must not be mutated.
|
||||
assert.Equal(t, "stale", req.Header.Get(SessionHeader))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionRoundTripper_StableAcrossRequests(t *testing.T) {
|
||||
capture := &captureRoundTripper{}
|
||||
rt := &SessionRoundTripper{Wrapped: capture, SessionID: NewSessionID()}
|
||||
|
||||
var first string
|
||||
for range 3 {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://example.com/api", nil)
|
||||
require.NoError(t, err)
|
||||
_, err = rt.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
got := capture.got.Header.Get(SessionHeader)
|
||||
require.NotEmpty(t, got)
|
||||
if first == "" {
|
||||
first = got
|
||||
} else {
|
||||
assert.Equal(t, first, got, "session ID must be stable across requests")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSessionID_UniqueAndNonEmpty(t *testing.T) {
|
||||
id1 := NewSessionID()
|
||||
id2 := NewSessionID()
|
||||
assert.NotEmpty(t, id1)
|
||||
assert.NotEqual(t, id1, id2, "NewSessionID must return a fresh ID per call")
|
||||
}
|
||||
Loading…
Reference in new issue