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")
}
}