Skip to content
quarry Artifact [3938ea465f]
Login

Artifact [3938ea465f]

Artifact 3938ea465f219121b9ed97626727345ce945dc1671075c4c8fffdaa2a820895f:


package api

import (
	"os"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/oltp"
)

// A repository with no bucket of its own inherits the FORGE's, so that
// a self-hosted forge is usable by somebody who has not opened an
// account with a storage provider. That inheritance is deliberate.
//
// What was not deliberate: bucketInfo copied the forge binding's
// endpoint, bucket name and access key id into the repository's answer,
// and the settings form pre-filled from them. Every repo admin on the
// forge was handed the address and key id of the operator's own storage
// account — which is how dmestas found his personal R2 holding a forum
// announcement's files (ticket 9b8b8fb6).
//
// A repo admin is not the forge operator.
func TestAForgeBucketDoesNotTellRepoAdminsWhereItIs(t *testing.T) {
	forge := oltp.BucketBinding{
		Endpoint: "https://a8d3b022875aef5e907b1cc9404376ad.r2.cloudflarestorage.com",
		Bucket:   "zeitforge-attachments", Region: "auto",
		AccessKey: "AKIAEXAMPLEKEYID", PublicBase: "https://files.example.org",
		LimitBytes: 100 << 20, Archives: true,
	}
	info := BucketInfo{Repo: "alpha"}
	info = bucketInfo("alpha", forge)
	info.Repo = "alpha"
	// This is the line the fix replaces; assert on what the repository
	// is allowed to be told instead.
	got := forgeAnswer("alpha", forge)

	if !got.Bound || !got.Forge {
		t.Errorf("FAIL: the repository is not told it has forge storage: %+v", got)
	} else {
		t.Log("ok: the repository is told its attachments go to the forge's storage")
	}
	if got.Limit != forge.LimitBytes {
		t.Errorf("FAIL: the per-file limit is withheld; an uploader needs it")
	} else {
		t.Log("ok: the limit it must keep under is still told")
	}
	for name, v := range map[string]string{
		"endpoint":   got.Endpoint,
		"bucket":     got.Bucket,
		"access key": got.AccessKey,
		"region":     got.Region,
		"publicBase": got.PublicBase,
	} {
		if v != "" {
			t.Errorf("FAIL: the forge bucket's %s reached a repo admin: %q", name, v)
		} else {
			t.Logf("ok: the forge bucket's %s is not disclosed", name)
		}
	}
	// And the unredacted shape is what a repository's OWN binding still
	// gets — the fix must not blind a steward to their own bucket.
	own := bucketInfo("alpha", forge)
	if own.Endpoint == "" || own.AccessKey == "" {
		t.Error("FAIL: a repository can no longer see its own binding")
	} else {
		t.Log("ok: a repository still sees its own binding in full")
	}
	_ = strings.TrimSpace
	_ = info
}

// And bucketFor must actually USE it. Testing forgeAnswer alone would
// pass forever while the raw binding went back into the inherited
// answer, which is the line that leaked.
func TestTheInheritedAnswerGoesThroughForgeAnswer(t *testing.T) {
	src, err := os.ReadFile("bucket.go")
	if err != nil {
		t.Fatal(err)
	}
	i := strings.Index(string(src), "case fromForge:")
	if i < 0 {
		t.Fatal("FAIL: the forge-inheritance branch is not where this guard looks any more")
	}
	block := string(src)[i : i+1400]
	end := strings.Index(block, "case owner != repo:")
	if end > 0 {
		block = block[:end]
	}
	if !strings.Contains(block, "forgeAnswer(repo, b)") {
		t.Error("FAIL: the inherited answer no longer goes through forgeAnswer")
	} else {
		t.Log("ok: the inherited answer is redacted at the one place it is built")
	}
}