Skip to content
quarry Artifact [218ee2105c]
Login

Artifact [218ee2105c]

Artifact 218ee2105ccb704fa49d6ef4aa026f909b4c3c31557755be044231ed2c0ddded:


package web

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

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/ci"
	"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
}

func (m *mintRecorder) RunDetail(string) (ci.Run, error) { return ci.Run{}, nil }
func (m *mintRecorder) StepLogs(string, string, 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 nil, nil }
func (m *mintRecorder) RevokeWorkerToken(string) error          { 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()
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "v",
		Repos: api.NewRepos(map[string]*api.Repo{"alpha": {}})}); 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 })
	srv := httptest.NewServer(HandlerWith(r, nil, nil, []Option{
		WithSessions(fakeSessions{user: "dan", roles: map[string]ops.Auth{"alpha": ops.Admin}}),
		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)
	}
}

// And a site admin still mints fleet-wide, prefixes and all. Nothing
// else can: RFC 0009's fleet Runners page does not exist yet, so this
// form is the only door to an unscoped token and closing it would
// strand the forge's own workers.
func TestASiteAdminStillMintsAFleetWideScope(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")

	resp := postAsDan(t, srv, "/r/alpha/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("a site admin was refused (status %d): %s", resp.StatusCode,
			strings.TrimSpace(string(body[:n])))
	}
	eng.mu.Lock()
	defer eng.mu.Unlock()
	if eng.minted != 1 {
		t.Fatalf("minted %d tokens, want 1", eng.minted)
	}
	if len(eng.prefixes) != 1 || eng.prefixes[0] != "dantest-puzzles" {
		t.Errorf("prefixes = %v, want the one asked for", eng.prefixes)
	}
	if len(eng.groups) != 0 {
		t.Errorf("groups = %v, want none: a typed scope leaves the token 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)
	}
}