cli/agent_auth_test.go
Rama main · réplica de lectura del repositorio original.
package cli
import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"time"
"github.com/spf13/cobra"
"go.keploy.io/server/v3/config"
"go.keploy.io/server/v3/internal/testnet"
"go.keploy.io/server/v3/pkg/agent/token"
"go.keploy.io/server/v3/utils"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest/observer"
)
// agentFlags registers the flags the agent's RunE reads straight off the
// command, as the real CmdConfigurator does.
type agentFlags struct{ noFlags }
func (agentFlags) AddFlags(cmd *cobra.Command) error {
cmd.Flags().Uint32("client-pid", 0, "")
cmd.Flags().Bool("is-docker", false, "")
cmd.Flags().String("token-file", "", "")
cmd.Flags().String("teardown-progress-file", "", "")
return nil
}
// servingAgent announces port the way a real agent does once it is hooked, and
// then runs until it is stopped. setupCalled records whether it got that far.
type servingAgent struct {
agentSvc
port int
setupCalled chan struct{}
}
func (s servingAgent) Setup(ctx context.Context, startCh chan int) error {
close(s.setupCalled)
select {
case startCh <- s.port:
case <-ctx.Done():
return nil
}
<-ctx.Done()
return context.Canceled
}
// runningAgent is the real `keploy agent` RunE, started in the background.
type runningAgent struct {
base string // http://127.0.0.1:<port>/agent
setupCalled chan struct{}
done chan struct{}
}
func startAgent(t *testing.T, logger *zap.Logger, args ...string) *runningAgent {
t.Helper()
port := testnet.ReservedPort(t) // no other socket is given it before the agent listens
utils.ErrCode = 0
t.Cleanup(func() { utils.ErrCode = 0 })
// The agent reports its teardown into this test's own directory -- one
// only its owner may write to, as a command-line path must be whatever the
// umask -- the way it would: a native agent where --teardown-progress-file
// says, a containerised one (--is-docker) where its container's path is,
// here pointed at that directory rather than the path every process on
// the host shares. Its reporter is stopped once the agent has returned
// (cleanups run last first), before the directory is removed.
dir := filepath.Join(t.TempDir(), "teardown")
if err := os.Mkdir(dir, 0o700); err != nil {
t.Fatal(err)
}
progress := filepath.Join(dir, utils.TeardownProgressFileName)
t.Cleanup(utils.StopTeardownReporting)
if slices.Contains(args, "--is-docker") {
orig := containerTeardownProgressFile
containerTeardownProgressFile = progress
t.Cleanup(func() { containerTeardownProgressFile = orig })
} else {
args = append([]string{"--teardown-progress-file", progress}, args...)
}
ctx, cancel := context.WithCancel(context.Background())
svc := servingAgent{port: port, setupCalled: make(chan struct{})}
cmd := Agent(ctx, logger, &config.Config{}, agentSvcFactory{svc: svc}, agentFlags{})
if err := cmd.ParseFlags(args); err != nil {
t.Fatal(err)
}
a := &runningAgent{base: fmt.Sprintf("http://127.0.0.1:%d/agent", port), setupCalled: svc.setupCalled, done: make(chan struct{})}
go func() {
defer close(a.done)
if err := cmd.RunE(cmd, nil); err != nil {
t.Errorf("RunE returned %v; the agent reports failure through its exit code", err)
}
}()
t.Cleanup(func() { cancel(); <-a.done })
return a
}
func (a *runningAgent) get(t *testing.T, path, bearer string) int {
t.Helper()
req, err := http.NewRequest(http.MethodGet, a.base+path, nil)
if err != nil {
t.Fatal(err)
}
if bearer != "" {
req.Header.Set("Authorization", "Bearer "+bearer)
}
resp, err := (&http.Client{Timeout: 2 * time.Second}).Do(req)
if err != nil {
return -1
}
_ = resp.Body.Close()
return resp.StatusCode
}
// waitServing waits until the agent answers its health check.
func (a *runningAgent) waitServing(t *testing.T) {
t.Helper()
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
if a.get(t, "/health", "") == http.StatusOK {
return
}
select {
case <-a.done:
t.Fatalf("the agent exited (code %d) instead of serving", utils.ErrCode)
case <-time.After(20 * time.Millisecond):
}
}
t.Fatal("the agent never started serving")
}
func writeTokenFile(t *testing.T, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "agent-token")
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
return path
}
// TestAgent_GuardsEveryRouteWithTheTokenItWasHanded runs the real agent command
// end to end. Every route test in pkg/agent/routes wraps Authenticate by hand,
// so deleting the one line that installs it on the real router left all of them
// green while the agent served its whole control plane to anyone.
func TestAgent_GuardsEveryRouteWithTheTokenItWasHanded(t *testing.T) {
t.Setenv(token.Env, "")
const tok = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
tokenFile := writeTokenFile(t, token.Env+"="+tok+"\n")
a := startAgent(t, zap.NewNop(), "--token-file", tokenFile)
a.waitServing(t)
// Wrong and missing tokens are refused before any route is matched: the
// probe path has no handler, so a 404 here would mean the guard let the
// request through.
if got := a.get(t, token.ProbePath, "not-the-token"); got != http.StatusUnauthorized {
t.Errorf("a wrong token got %d, want 401", got)
}
if got := a.get(t, "/mock/stats", ""); got != http.StatusUnauthorized {
t.Errorf("no token got %d on a registered route, want 401", got)
}
// The token the launcher handed over gets past the guard to the router,
// which has no such route.
if got := a.get(t, token.ProbePath, tok); got != http.StatusNotFound {
t.Errorf("the handed-over token got %d, want the router's 404", got)
}
if _, err := os.Stat(tokenFile); !os.IsNotExist(err) {
t.Error("the agent left the session token on disk after reading it")
}
}
// TestAgent_RefusesToStartWithATokenFileItCannotRead pins failing closed. It
// used to fall back to the environment and, finding nothing there, serve its
// whole control plane open — the outcome a planted file was after.
func TestAgent_RefusesToStartWithATokenFileItCannotRead(t *testing.T) {
t.Setenv(token.Env, "")
core, logs := observer.New(zapcore.ErrorLevel)
a := startAgent(t, zap.New(core), "--token-file", writeTokenFile(t, "garbage\n"))
select {
case <-a.done:
case <-time.After(10 * time.Second):
t.Fatal("the agent did not exit")
}
if utils.ErrCode == 0 {
t.Error("the agent exited 0; the CLI that launched it would read that as a clean finish")
}
select {
case <-a.setupCalled:
t.Error("the agent went on to hook the application with a token it could not read")
default:
}
if got := a.get(t, "/health", ""); got != -1 {
t.Errorf("something is serving the agent port (%d)", got)
}
found := false
for _, e := range logs.All() {
reason, _ := e.ContextMap()["error"].(string)
found = found || strings.Contains(reason, "will not start")
}
if !found {
t.Error("the agent exited without saying why")
}
}
// TestAgent_DaemonSetSaysNothingAboutAServerItNeverStarts: a DaemonSet agent
// never binds the control-plane server, so the startup warning that its
// control plane is unauthenticated was a false alarm in every node's log.
func TestAgent_DaemonSetSaysNothingAboutAServerItNeverStarts(t *testing.T) {
t.Setenv(token.Env, "")
t.Setenv("KEPLOY_DAEMONSET_ENABLED", "true")
core, logs := observer.New(zapcore.InfoLevel)
a := startAgent(t, zap.New(core))
deadline := time.Now().Add(10 * time.Second)
for logs.FilterMessageSnippet("not starting the control-plane HTTP server").Len() == 0 {
if time.Now().After(deadline) {
t.Fatal("the DaemonSet agent never reached its server gate")
}
time.Sleep(20 * time.Millisecond)
}
if n := logs.FilterMessageSnippet("authentication").Len(); n != 0 {
t.Errorf("the DaemonSet agent reported on the authentication of a server it never starts: %v",
logs.FilterMessageSnippet("authentication").All()[0].Message)
}
if got := a.get(t, "/health", ""); got != -1 {
t.Errorf("the DaemonSet agent is serving its control plane (%d)", got)
}
}
// TestAgent_InClusterAgentSaysPlainlyWhyItIsUnauthenticated starts the agent
// the way Kubernetes runs a pod agent that k8s-proxy handed no token: a
// container (--is-docker) in a pod, with no KEPLOY_AGENT_TOKEN and no
// KEPLOY_DAEMONSET_ENABLED, as one injected before k8s-proxy's per-sidecar
// token release, a replay or sandbox pod in a DaemonSet install, or one
// injected with proxy.keployAgentControlPlaneAuth=false is. It still serves,
// and says so once, plainly, naming those three causes — not with the
// local-handoff warning, whose remedies do not exist in a pod.
func TestAgent_InClusterAgentSaysPlainlyWhyItIsUnauthenticated(t *testing.T) {
t.Setenv(token.Env, "")
t.Setenv("KUBERNETES_SERVICE_HOST", "10.96.0.1")
core, logs := observer.New(zapcore.InfoLevel)
a := startAgent(t, zap.New(core), "--is-docker")
a.waitServing(t)
said := logs.FilterMessageSnippet("authentication").All()
if len(said) != 1 {
t.Fatalf("the in-cluster agent said %d things about its authentication, want exactly one", len(said))
}
if said[0].Level != zapcore.InfoLevel ||
!strings.Contains(said[0].Message, "this in-cluster agent was started without a control-plane token") ||
!strings.Contains(said[0].Message, "pods injected before that release") ||
!strings.Contains(said[0].Message, "pods in a DaemonSet install") ||
!strings.Contains(said[0].Message, "proxy.keployAgentControlPlaneAuth=false") ||
strings.Contains(said[0].Message, "trusted") {
t.Errorf("the in-cluster agent said, at %s: %s", said[0].Level, said[0].Message)
}
}
// TestAgent_InClusterAgentEnforcesTheTokenK8sProxyHandedIt starts the agent the
// way Kubernetes runs a sidecar k8s-proxy injected with a token: a container
// (--is-docker) in a pod, with KEPLOY_AGENT_TOKEN in its environment. It must
// guard every route with that token and say nothing about authentication.
func TestAgent_InClusterAgentEnforcesTheTokenK8sProxyHandedIt(t *testing.T) {
const tok = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
t.Setenv(token.Env, tok)
t.Setenv("KUBERNETES_SERVICE_HOST", "10.96.0.1")
core, logs := observer.New(zapcore.InfoLevel)
a := startAgent(t, zap.New(core), "--is-docker")
a.waitServing(t)
if got := a.get(t, "/mock/stats", ""); got != http.StatusUnauthorized {
t.Errorf("no token got %d on a registered route, want 401", got)
}
if got := a.get(t, token.ProbePath, "not-the-token"); got != http.StatusUnauthorized {
t.Errorf("a wrong token got %d, want 401", got)
}
if got := a.get(t, token.ProbePath, tok); got != http.StatusNotFound {
t.Errorf("the token k8s-proxy handed over got %d, want the router's 404", got)
}
if said := logs.FilterMessageSnippet("authentication").All(); len(said) != 0 {
t.Errorf("the in-cluster agent enforcing its token said, at %s: %s", said[0].Level, said[0].Message)
}
}
// TestRoot_RemovesTheAgentTokenDirectoryWhenTheCommandFinishes: the private
// directory token.WriteFile creates must not outlive the command. Removal used
// to be a defer in this repository's main, which the enterprise binary — with
// a main of its own, but commands built by this same Root — never runs.
func TestRoot_RemovesTheAgentTokenDirectoryWhenTheCommandFinishes(t *testing.T) {
root := Root(context.Background(), zap.NewNop(), agentSvcFactory{}, noFlags{})
var tokenFile string
root.AddCommand(&cobra.Command{
Use: "launch-an-agent",
RunE: func(*cobra.Command, []string) error {
var err error
tokenFile, err = token.WriteFile()
if err != nil {
return err
}
// However the command ends, error included.
return fmt.Errorf("the run failed")
},
})
root.SetArgs([]string{"launch-an-agent"})
root.SilenceErrors, root.SilenceUsage = true, true
if err := root.Execute(); err == nil {
t.Fatal("the command's error was lost")
}
if tokenFile == "" {
t.Fatal("the command never wrote a token file")
}
if _, err := os.Stat(filepath.Dir(tokenFile)); !os.IsNotExist(err) {
t.Errorf("the agent token directory %s outlived the command", filepath.Dir(tokenFile))
}
}