Skip to content
quarry Artifact [33d9c755a6]
Login

Artifact [33d9c755a6]

Artifact 33d9c755a6cd01aa5e4e0078d2fe685817998d530828c688713c5fcc0d0853fd:


package runner

import (
	"context"
	"encoding/json"
	"net/http/httptest"
	"os"
	"os/user"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

// The test binary stands in for `quarry job-env` when the fake sudo
// below starts it with QUARRY_TEST_JOBENV set.
func TestMain(m *testing.M) {
	if os.Getenv("QUARRY_TEST_JOBENV") == "1" {
		if err := JobEnvMain(os.Args[2:]); err != nil {
			os.Stderr.WriteString(err.Error() + "\n")
			os.Exit(1)
		}
		return
	}
	os.Exit(m.Run())
}

// A JOB RUNS AS ITS SLOT'S USER WITH ONLY ITS OWN ENVIRONMENT, AND GETS
// A SECRET ONLY FROM THE FORGE, MASKED IN THE LOG (RFC 0019 §isolation).
//
// sudo is a fake here that runs the command as the current user,
// keeping every argument but the user switch, and never runs pkill: the
// real reclaim kills the slot user's processes, and on a CI box the
// slot group's name can be the CI user's own.
func TestAJobRunsAsItsSlotWithOnlyItsEnvironment(t *testing.T) {
	u, err := user.Current()
	if err != nil {
		t.Fatal(err)
	}
	g, err := user.LookupGroupId(u.Gid)
	if err != nil {
		t.Skipf("no group name for gid %s: %v", u.Gid, err)
	}
	bin := t.TempDir()
	fake := "#!/bin/sh\n# -n -u SLOT -- CMD...\n[ \"$5\" = pkill ] && exit 0\nshift 4\nexec env QUARRY_TEST_JOBENV=1 \"$@\"\n"
	if err := os.WriteFile(filepath.Join(bin, "sudo"), []byte(fake), 0o755); err != nil {
		t.Fatal(err)
	}
	t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
	t.Setenv("QUARRY_RUNNER_TOKEN", "must-not-reach-the-job")
	self, err := os.Executable()
	if err != nil {
		t.Fatal(err)
	}

	job := func(j Job) map[string]string {
		b, _ := json.Marshal(j)
		return map[string]string{JobKey: string(b)}
	}
	with := func(id string, meta map[string]string) polled {
		p := task(id, "c1")
		p.Metadata = meta
		return p
	}
	b := &fakeBridge{queue: map[string][]polled{"t-job": {
		with("deploy", job(Job{Env: map[string]string{"STAGE": "prod"}, Secrets: []string{"CF_TOKEN"}, Steps: []JobStep{
			{Name: "look", Run: "echo token=$CF_TOKEN stage=$STAGE runner=${QUARRY_RUNNER_TOKEN:-absent} step=${STEP_ONLY:-absent}"},
			{Name: "own", Run: "echo step=$STEP_ONLY", Secrets: []string{"STEP_ONLY"}},
			{Name: "umask", Run: "touch made && ls -l made | cut -c1-10"},
		}})),
		with("refused", job(Job{Secrets: []string{"CF_TOKEN"}, Steps: []JobStep{{Run: "echo ran"}}})),
		with("plain", job(Job{Steps: []JobStep{{Run: "echo runner=${QUARRY_RUNNER_TOKEN:-absent}"}}})),
	}}, resolved: map[string]map[string]any{}, logs: map[string]string{}}
	b.secrets = func(taskID string, names []string) (map[string]string, int) {
		if taskID == "refused" {
			return nil, 403
		}
		return map[string]string{"CF_TOKEN": "cf-s3cr3t-value", "STEP_ONLY": "step-s3cr3t"}, 200
	}
	srv := httptest.NewServer(b.handler())
	defer srv.Close()
	r, err := New(Config{
		Bridge: srv.URL, Token: "WT", Tasks: []string{"t-job"}, MaxTasks: 1,
		WorkDir: t.TempDir(), Checkout: appCheckout{}, Heartbeat: 50 * time.Millisecond, Logf: t.Logf,
		JobUsers: []string{g.Name}, Self: self,
	})
	if err != nil {
		t.Fatal(err)
	}
	ctx, cancel := context.WithCancel(context.Background())
	done := make(chan struct{})
	go func() { r.Run(ctx); close(done) }()
	for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) {
		b.mu.Lock()
		n := len(b.resolved)
		b.mu.Unlock()
		if n == 3 {
			break
		}
	}
	cancel()
	<-done
	b.mu.Lock()
	defer b.mu.Unlock()
	if got := b.resolved["deploy"]; got["action"] != "complete" {
		t.Fatalf("FAIL: the deploy resolved %v, log %q", got, b.logs["deploy"])
	}
	log := b.logs["deploy"]
	if strings.Contains(log, "s3cr3t") {
		t.Errorf("FAIL: a secret value reached the log: %q", log)
	}
	if !strings.Contains(log, "token=*** stage=prod runner=absent step=absent") {
		t.Errorf("FAIL: the first step saw %q; want the job's secret (masked) and env, no runner token, no step-only secret", log)
	}
	if !strings.Contains(log, "step=***") {
		t.Errorf("FAIL: the step with its own secret did not get it: %q", log)
	}
	if !strings.Contains(log, "-rw-rw----") {
		t.Errorf("FAIL: what the job wrote is not private to its group: %q", log)
	}
	if got := b.resolved["refused"]; got["action"] != "fail" || !strings.Contains(got["error"].(string), "secrets refused") || strings.Contains(b.logs["refused"], "ran") {
		t.Errorf("FAIL: a refused delivery resolved %v; want the task failed with the forge's words, unrun", got)
	}
	if !strings.Contains(b.logs["plain"], "runner=absent") {
		t.Errorf("FAIL: a job on an isolating runner saw the runner's token: %q", b.logs["plain"])
	}
	t.Log("ok: slot user, own env only, secrets scoped and masked, refusals fail the task")
}

// Masking catches a value split across writes and each line of a
// multi-line one.
func TestTheMaskCatchesSplitAndMultilineValues(t *testing.T) {
	var out strings.Builder
	m := newMasker(&out, map[string]string{"K": "abcdef123", "PEM": "-----BEGIN-----\nsecretline\n-----END-----"})
	m.Write([]byte("value abc"))
	m.Write([]byte("def123 end\nkey:\nsecretline\n"))
	m.Write([]byte("tail abcdef123"))
	m.Close()
	if s := out.String(); strings.Contains(s, "abcdef123") || strings.Contains(s, "secretline") || !strings.Contains(s, "value *** end") || !strings.HasSuffix(s, "tail ***") {
		t.Errorf("FAIL: masked output %q", s)
	}
}