Skip to content
quarry Artifact [7608f79675]
Login

Artifact [7608f79675]

Artifact 7608f79675b0709b4aa13ea08120494784f0734b55e98016b9d5bcef646f93fe:


package web

import (
	"net/http"
	"net/http/httptest"
	"net/url"
	"path/filepath"
	"strings"
	"sync"
	"testing"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/ci"
	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/ops"
)

// mintRecorder answers the engine surface and remembers what a mint was
// actually asked for, which is the only way to tell a token scoped as
// requested from one scoped to something else.
type mintRecorder struct {
	mu       sync.Mutex
	minted   int
	prefixes []string
	groups   []string
	tokens   []ci.WorkerToken
	revoked  []string
}

func (m *mintRecorder) RunDetail(string) (ci.Run, error)                  { return ci.Run{}, nil }
func (m *mintRecorder) StepLimits(string, string) (map[string]int, error) { return nil, nil }

func (m *mintRecorder) StepLogs(string, string, int, int, bool) ([]ci.LogChunk, error) {
	return nil, nil
}
func (m *mintRecorder) Workers() ([]ci.Worker, error)           { return nil, nil }
func (m *mintRecorder) WorkerTokens() ([]ci.WorkerToken, error) { return m.tokens, nil }
func (m *mintRecorder) RevokeWorkerToken(id string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.revoked = append(m.revoked, id)
	return nil
}
func (m *mintRecorder) MintWorkerToken(_ string, prefixes, groups []string) (ci.WorkerToken, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.minted++
	m.prefixes, m.groups = prefixes, groups
	return ci.WorkerToken{ID: "tok1", Token: "s"}, nil
}

func scopeServer(t *testing.T, eng *mintRecorder, letters string) *httptest.Server {
	t.Helper()
	// An empty but real index: a refusal's page reads the repository's
	// counts for its chrome, and every production repository has one.
	ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "v",
		Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}})}); err != nil {
		t.Fatal(err)
	}
	// A Verify must be present or mayCap short-circuits to true and no
	// capability is checked at all -- which is exactly how this test
	// first passed against the unfixed code.
	verify := Verify(func(string, string, string, ops.Auth) api.Decision { return api.Allowed })
	roles := map[string]ops.Auth{"alpha": ops.Admin}
	if strings.Contains(letters, "s") {
		roles[""] = ops.Admin // a site admin reaches the fleet pages
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil, []Option{
		WithSessions(fakeSessions{user: "dan", roles: roles}),
		WithCaps(stubCaps{letters: letters}),
		WithEngine(eng),
	}, verify))
	t.Cleanup(srv.Close)
	return srv
}

// A scope the caller may not set is REFUSED, not dropped.
//
// The capability check and the field check were one condition, so a
// repository admin who posted `prefixes` had them silently ignored and
// got a group-scoped token back. Nothing said the field went nowhere.
// jva hit it with a stale form and read the three tokens as correctly
// scoped -- which they were, just not scoped to what was asked for
// (2c164754).
//
// Asserted on what reached the ENGINE, not on the status code alone: a
// refusal that still minted something would be the same bug wearing an
// error message.
func TestAScopeYouMayNotSetIsRefusedNotDropped(t *testing.T) {
	eng := &mintRecorder{}
	// `a` and `8`, not `s`: a repository admin, which is the account
	// the field is not drawn for.
	srv := scopeServer(t, eng, "a8i")

	resp := postAsDan(t, srv, "/r/alpha/settings/worker-tokens", url.Values{
		"label":    {"jva's runner"},
		"prefixes": {"dantest-puzzles dantest-html"},
	})
	if resp.StatusCode == http.StatusSeeOther {
		t.Errorf("a repository admin's task-type scope was accepted with a redirect: the "+
			"field was dropped and a differently-scoped token minted (status %d)", resp.StatusCode)
	}
	eng.mu.Lock()
	minted, prefixes := eng.minted, eng.prefixes
	eng.mu.Unlock()
	if minted != 0 {
		t.Errorf("a token was minted anyway (%d), scoped to %v -- the caller asked for a "+
			"fleet-wide credential and would have been handed a group one", minted, prefixes)
	}
}

// A site admin mints a fleet-wide token, prefixes and no group, on the
// fleet settings page; a repository's page refuses a prefix even from a
// site admin, so it deals only in its own group's tokens.
func TestASiteAdminMintsFleetWideOnTheFleetPage(t *testing.T) {
	eng := &mintRecorder{}
	// "as": stubCaps is naive and does not expand s the way a real
	// CapSet does, so the site admin needs the repository letter
	// spelled out to pass the route's own gate.
	srv := scopeServer(t, eng, "as")

	if resp := postAsDan(t, srv, "/r/alpha/settings/worker-tokens", url.Values{
		"label": {"forge runner"}, "prefixes": {"dantest-puzzles"},
	}); resp.StatusCode == http.StatusSeeOther {
		t.Fatal("FAIL: a repository's page minted a fleet-wide token")
	}
	resp := postAsDan(t, srv, "/fleet/settings/worker-tokens", url.Values{
		"label": {"forge runner"}, "prefixes": {"dantest-puzzles"},
	})
	if resp.StatusCode != http.StatusSeeOther {
		body := make([]byte, 400)
		n, _ := resp.Body.Read(body)
		t.Fatalf("FAIL: a site admin was refused on the fleet page (status %d): %s", resp.StatusCode,
			strings.TrimSpace(string(body[:n])))
	}
	eng.mu.Lock()
	defer eng.mu.Unlock()
	if eng.minted != 1 {
		t.Fatalf("FAIL: minted %d tokens, want 1", eng.minted)
	}
	if len(eng.prefixes) != 1 || eng.prefixes[0] != "dantest-puzzles" {
		t.Errorf("FAIL: prefixes = %v, want the one asked for", eng.prefixes)
	}
	if len(eng.groups) != 0 {
		t.Errorf("FAIL: groups = %v, want none: a fleet-wide token is ungrouped", eng.groups)
	}
}

// A repository admin who posts NO scope mints a group-scoped token whose
// task-type scope is the group's own name. This test used to assert the
// scope was EMPTY — which is exactly what made every such token useless:
// the engine reads an empty scope as "no task types at all" and refuses
// every poll (found running quarry runner against runner-probe,
// f24d78d9).
func TestARepoAdminStillMintsAGroupToken(t *testing.T) {
	eng := &mintRecorder{}
	srv := scopeServer(t, eng, "a8i")

	if resp := postAsDan(t, srv, "/r/alpha/settings/worker-tokens", url.Values{
		"label": {"repo runner"},
	}); resp.StatusCode != http.StatusSeeOther {
		t.Fatalf("an ordinary mint was refused: status %d", resp.StatusCode)
	}
	eng.mu.Lock()
	defer eng.mu.Unlock()
	if eng.minted != 1 || len(eng.groups) != 1 || len(eng.prefixes) != 1 || eng.prefixes[0] != eng.groups[0] {
		t.Errorf("minted=%d prefixes=%v groups=%v, want one token scoped to its group and the group's own task types",
			eng.minted, eng.prefixes, eng.groups)
	}
}