Made security review, applied recommendations.
This commit is contained in:
1 parent
630375c7ed
commit
1d4ca3da46
7 files changed
+363
-70
No files matched your search
+262
-49
@@ -205,10 +205,80 @@ func TestToken(t *testing.T) {
|
||||
if !slices.Equal(scopes, test.wantScopes) {
|
||||
t.Errorf("scopes = %v, want %v", scopes, test.wantScopes)
|
||||
}
|
||||
if got := recorder.Header().Get("Content-Type"); got != "application/json" {
|
||||
t.Errorf("Content-Type = %q, want application/json", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseScopes(t *testing.T) {
|
||||
tests := []struct {
|
||||
value string
|
||||
want []string
|
||||
}{
|
||||
{"", []string{defaultScope}},
|
||||
{",,", []string{defaultScope}},
|
||||
{"a", []string{"a"}},
|
||||
{"a,,b,", []string{"a", "b"}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if got := parseScopes(test.value); !slices.Equal(got, test.want) {
|
||||
t.Errorf("parseScopes(%q) = %v, want %v", test.value, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestLogOmitsQuery(t *testing.T) {
|
||||
var logged bytes.Buffer
|
||||
log.SetOutput(&logged)
|
||||
defer log.SetOutput(io.Discard)
|
||||
|
||||
get(newTestServer(nil), root+"instance/service-accounts/default/token?scopes=secret-scope")
|
||||
if !strings.Contains(logged.String(), "GET "+root+"instance/service-accounts/default/token 200") {
|
||||
t.Errorf("the request was not logged:\n%s", logged.String())
|
||||
}
|
||||
if strings.Contains(logged.String(), "secret-scope") {
|
||||
t.Errorf("the log contains the query:\n%s", logged.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadHasNoBody(t *testing.T) {
|
||||
server := httptest.NewServer(newTestServer(nil))
|
||||
defer server.Close()
|
||||
|
||||
request, _ := http.NewRequest(http.MethodHead, server.URL+root+"project/project-id", nil)
|
||||
request.Header.Set("Metadata-Flavor", "Google")
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
if response.StatusCode != 200 || len(body) != 0 {
|
||||
t.Errorf("got %d with body %q, want 200 without body", response.StatusCode, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
emulator := httptest.NewServer(newTestServer(nil))
|
||||
defer emulator.Close()
|
||||
other := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
defer other.Close()
|
||||
closed := httptest.NewServer(nil)
|
||||
closed.Close()
|
||||
|
||||
if err := ping(strings.TrimPrefix(emulator.URL, "http://")); err != nil {
|
||||
t.Errorf("ping(emulator) = %v, want nil", err)
|
||||
}
|
||||
if err := ping(strings.TrimPrefix(other.URL, "http://")); err == nil {
|
||||
t.Error("ping(other server) = nil, want an error")
|
||||
}
|
||||
if err := ping(strings.TrimPrefix(closed.URL, "http://")); err == nil {
|
||||
t.Error("ping(closed) = nil, want an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenExpiry(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -315,6 +385,20 @@ func TestProviderCache(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderCacheBound(t *testing.T) {
|
||||
cache := &providerCache{detect: func([]string) (auth.TokenProvider, error) {
|
||||
return tokenProviderFunc(func(context.Context) (*auth.Token, error) { return &auth.Token{Value: "t"}, nil }), nil
|
||||
}}
|
||||
for i := range 10 * maxProviders {
|
||||
if _, err := cache.token(context.Background(), []string{strings.Repeat("s", i+1)}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(cache.providers) > maxProviders {
|
||||
t.Fatalf("%d providers cached after %d scope sets, want at most %d", len(cache.providers), i+1, maxProviders)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderCacheConcurrent(t *testing.T) {
|
||||
var detected atomic.Int32
|
||||
cache := &providerCache{detect: func([]string) (auth.TokenProvider, error) {
|
||||
@@ -497,6 +581,98 @@ func writeServiceAccountKey(t *testing.T) string {
|
||||
return path
|
||||
}
|
||||
|
||||
// safeBuffer collects the output of a process while it runs.
|
||||
type safeBuffer struct {
|
||||
mu sync.Mutex
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *safeBuffer) Write(p []byte) (int, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.buf.Write(p)
|
||||
}
|
||||
|
||||
func (b *safeBuffer) String() string {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.buf.String()
|
||||
}
|
||||
|
||||
// emulator is the test binary running as the emulator process.
|
||||
type emulator struct {
|
||||
address string
|
||||
process *os.Process
|
||||
exited <-chan error
|
||||
output *safeBuffer
|
||||
}
|
||||
|
||||
// freePort returns a free TCP port of the loopback interface.
|
||||
func freePort(t *testing.T) string {
|
||||
t.Helper()
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
_, port, _ := net.SplitHostPort(listener.Addr().String())
|
||||
return port
|
||||
}
|
||||
|
||||
// runEmulator starts the test binary as the emulator with the arguments and
|
||||
// the environment on the loopback interface, and returns it once it answers.
|
||||
func runEmulator(t *testing.T, env []string, args ...string) *emulator {
|
||||
t.Helper()
|
||||
port := freePort(t)
|
||||
e := &emulator{address: "127.0.0.1:" + port, output: &safeBuffer{}}
|
||||
|
||||
command := exec.Command(os.Args[0], args...)
|
||||
command.Env = append(os.Environ(), mainEnv+"=1", "GOOGLE_APPLICATION_CREDENTIALS="+writeServiceAccountKey(t), "IMDS_HOST=127.0.0.1", "IMDS_PORT="+port)
|
||||
command.Env = append(command.Env, env...)
|
||||
command.Stderr = e.output
|
||||
if err := command.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
exited := make(chan error, 1)
|
||||
go func() { exited <- command.Wait() }()
|
||||
e.process, e.exited = command.Process, exited
|
||||
t.Cleanup(func() { command.Process.Kill() })
|
||||
|
||||
for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline); {
|
||||
select {
|
||||
case err := <-exited:
|
||||
t.Fatalf("the emulator exited before it was ready: %v\n%s", err, e.output.String())
|
||||
default:
|
||||
}
|
||||
if ping(e.address) == nil {
|
||||
return e
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("the emulator did not answer\n%s", e.output.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
// wait returns the exit error of the emulator, failing the test when it does not stop.
|
||||
func (e *emulator) wait(t *testing.T) error {
|
||||
t.Helper()
|
||||
select {
|
||||
case err := <-e.exited:
|
||||
return err
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatalf("the emulator did not stop\n%s", e.output.String())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// runCheck runs the test binary in check mode against the port and returns its exit error.
|
||||
func runCheck(t *testing.T, port string) error {
|
||||
t.Helper()
|
||||
command := exec.Command(os.Args[0], "-check")
|
||||
command.Env = append(os.Environ(), mainEnv+"=1", "IMDS_PORT="+port)
|
||||
return command.Run()
|
||||
}
|
||||
|
||||
// TestSignals runs the emulator as a process and stops it with signals.
|
||||
func TestSignals(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
@@ -504,59 +680,96 @@ func TestSignals(t *testing.T) {
|
||||
}
|
||||
for _, signal := range []syscall.Signal{syscall.SIGTERM, syscall.SIGINT} {
|
||||
t.Run(signal.String(), func(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
e := runEmulator(t, nil)
|
||||
if err := e.process.Signal(signal); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
address := listener.Addr().String()
|
||||
_, port, _ := net.SplitHostPort(address)
|
||||
listener.Close()
|
||||
|
||||
var output bytes.Buffer
|
||||
command := exec.Command(os.Args[0])
|
||||
command.Env = append(os.Environ(), mainEnv+"=1", "GOOGLE_APPLICATION_CREDENTIALS="+writeServiceAccountKey(t), "IMDS_PORT="+port)
|
||||
command.Stderr = &output
|
||||
if err := command.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
if err := e.wait(t); err != nil {
|
||||
t.Errorf("the emulator exited with %v, want status 0\n%s", err, e.output.String())
|
||||
}
|
||||
exited := make(chan error, 1)
|
||||
go func() { exited <- command.Wait() }()
|
||||
t.Cleanup(func() { command.Process.Kill() })
|
||||
|
||||
// Wait until the emulator answers the ping.
|
||||
ready := false
|
||||
for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline) && !ready; {
|
||||
select {
|
||||
case err := <-exited:
|
||||
t.Fatalf("the emulator exited before it was ready: %v\n%s", err, output.String())
|
||||
default:
|
||||
}
|
||||
request, _ := http.NewRequest(http.MethodGet, "http://"+address+"/", nil)
|
||||
request.Header.Set("Metadata-Flavor", "Google")
|
||||
if response, err := http.DefaultClient.Do(request); err == nil {
|
||||
response.Body.Close()
|
||||
ready = response.StatusCode == http.StatusOK
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
if !ready {
|
||||
t.Fatalf("the emulator did not answer\n%s", output.String())
|
||||
}
|
||||
|
||||
if err := command.Process.Signal(signal); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case err := <-exited:
|
||||
if err != nil {
|
||||
t.Errorf("the emulator exited with %v, want status 0\n%s", err, output.String())
|
||||
}
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatalf("the emulator did not stop\n%s", output.String())
|
||||
}
|
||||
if !strings.Contains(output.String(), "shutting down") || !strings.Contains(output.String(), "Stopped") {
|
||||
t.Errorf("unexpected log:\n%s", output.String())
|
||||
if !strings.Contains(e.output.String(), "shutting down") || !strings.Contains(e.output.String(), "Stopped") {
|
||||
t.Errorf("unexpected log:\n%s", e.output.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("second signal", func(t *testing.T) {
|
||||
e := runEmulator(t, nil)
|
||||
|
||||
// A partial request keeps a connection active, so the shutdown waits for it.
|
||||
connection, err := net.Dial("tcp", e.address)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer connection.Close()
|
||||
if _, err := io.WriteString(connection, "GET / HTTP/1.1\r\nHost: imds\r\n"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := e.process.Signal(syscall.SIGTERM); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case err := <-e.exited:
|
||||
t.Fatalf("the emulator exited with %v while a connection was active", err)
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
}
|
||||
if err := e.process.Signal(syscall.SIGTERM); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var exitError *exec.ExitError
|
||||
if err := e.wait(t); !errors.As(err, &exitError) || exitError.ExitCode() != 1 {
|
||||
t.Errorf("the emulator exited with %v, want status 1\n%s", err, e.output.String())
|
||||
}
|
||||
if !strings.Contains(e.output.String(), "again, exiting") {
|
||||
t.Errorf("unexpected log:\n%s", e.output.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestProcess checks the environment handling and the check mode of the emulator process.
|
||||
func TestProcess(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("POSIX signals")
|
||||
}
|
||||
email := func(t *testing.T, address string) string {
|
||||
t.Helper()
|
||||
request, _ := http.NewRequest(http.MethodGet, "http://"+address+root+"instance/service-accounts/default/email", nil)
|
||||
request.Header.Set("Metadata-Flavor", "Google")
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
return string(body)
|
||||
}
|
||||
|
||||
t.Run("email from credentials", func(t *testing.T) {
|
||||
e := runEmulator(t, nil)
|
||||
if got := email(t, e.address); got != testEmail {
|
||||
t.Errorf("email = %q, want %q", got, testEmail)
|
||||
}
|
||||
if !strings.Contains(e.output.String(), "Serving tokens of "+testEmail+" for project dom-lab on "+e.address) {
|
||||
t.Errorf("unexpected log:\n%s", e.output.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("email override", func(t *testing.T) {
|
||||
e := runEmulator(t, []string{"SERVICE_ACCOUNT_EMAIL=other@dom-lab.iam.gserviceaccount.com"})
|
||||
if got := email(t, e.address); got != "other@dom-lab.iam.gserviceaccount.com" {
|
||||
t.Errorf("email = %q, want the override", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("check", func(t *testing.T) {
|
||||
e := runEmulator(t, nil)
|
||||
_, port, _ := net.SplitHostPort(e.address)
|
||||
if err := runCheck(t, port); err != nil {
|
||||
t.Errorf("check against the emulator = %v, want nil", err)
|
||||
}
|
||||
if err := runCheck(t, freePort(t)); err == nil {
|
||||
t.Error("check against a closed port = nil, want an error")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user