package cibridge
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// A RUNNER GETS SECRETS ONLY FOR A TASK IT HOLDS, AND ONLY WHAT THE
// FORGE'S RULE ALLOWS (RFC 0019 §secrets).
//
// The proof of holding is the engine accepting a heartbeat for the task
// with the runner's own token; the fake engine here accepts it for
// "CLAIMER" only, as dagnats#627 does for the token that claimed it.
func TestSecretsGoOnlyToTheRunnerHoldingTheTask(t *testing.T) {
var heartbeats []string
engine := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/tasks/run1.deploy/resolve" {
http.NotFound(w, r)
return
}
var body map[string]string
json.NewDecoder(r.Body).Decode(&body)
heartbeats = append(heartbeats, body["action"])
if r.Header.Get("Authorization") != "Bearer CLAIMER" {
http.Error(w, "task not claimed by this token", http.StatusForbidden)
return
}
w.WriteHeader(http.StatusOK)
}))
defer engine.Close()
var asked []string
deliver := func(repo, taskID, env string, names []string) (map[string]string, error) {
asked = append(asked, repo+" "+taskID+" "+env+" "+strings.Join(names, ","))
if env == "staging" {
return nil, errors.New("secrets refused: TOKEN goes to protected branches only, and this check-in is on \"feat\"")
}
return map[string]string{"TOKEN": "v"}, nil
}
h, err := Handler(engine.URL, "ADMIN", func(string) string { return "g" }, func(r string) bool { return r == "alpha" }, deliver)
if err != nil {
t.Fatal(err)
}
ask := func(repo, token, body string) *httptest.ResponseRecorder {
req := httptest.NewRequest("POST", "/r/"+repo+"/ci/bridge/v1/tasks/run1.deploy/secrets", strings.NewReader(body))
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
ok := `{"environment":"production","names":["TOKEN"]}`
if rec := ask("alpha", "CLAIMER", ok); rec.Code != 200 || !strings.Contains(rec.Body.String(), `"TOKEN":"v"`) || rec.Header().Get("Cache-Control") != "no-store" {
t.Errorf("FAIL: the claiming runner got %d %q (cache %q); want the value, uncached", rec.Code, rec.Body, rec.Header().Get("Cache-Control"))
}
if rec := ask("alpha", "OTHER", ok); rec.Code != http.StatusForbidden || strings.Contains(rec.Body.String(), `"v"`) {
t.Errorf("FAIL: a token that did not claim the task got %d %q; want 403, no value", rec.Code, rec.Body)
}
if rec := ask("alpha", "ADMIN", ok); rec.Code != http.StatusForbidden {
t.Errorf("FAIL: the engine's admin token got %d; want 403", rec.Code)
}
if rec := ask("alpha", "", ok); rec.Code != http.StatusUnauthorized {
t.Errorf("FAIL: no token got %d; want 401", rec.Code)
}
if rec := ask("beta", "CLAIMER", ok); rec.Code != http.StatusNotFound {
t.Errorf("FAIL: an unknown repository got %d; want 404", rec.Code)
}
if rec := ask("alpha", "CLAIMER", `{"names":[]}`); rec.Code != http.StatusBadRequest {
t.Errorf("FAIL: no names got %d; want 400", rec.Code)
}
if rec := ask("alpha", "CLAIMER", `{"environment":"staging","names":["TOKEN"]}`); rec.Code != http.StatusForbidden || !strings.Contains(rec.Body.String(), "protected branches only") {
t.Errorf("FAIL: a refused delivery got %d %q; want 403 saying why", rec.Code, rec.Body)
}
if len(asked) != 2 || asked[0] != "alpha run1.deploy production TOKEN" {
t.Errorf("FAIL: the rule was asked %v; want only for the claiming runner's requests", asked)
}
for _, a := range heartbeats {
if a != "heartbeat" {
t.Errorf("FAIL: the proof sent action %q to the engine; want only heartbeats", a)
}
}
h2, _ := Handler(engine.URL, "ADMIN", func(string) string { return "g" }, func(string) bool { return true }, nil)
req := httptest.NewRequest("POST", "/r/alpha/ci/bridge/v1/tasks/run1.deploy/secrets", strings.NewReader(ok))
req.Header.Set("Authorization", "Bearer CLAIMER")
rec := httptest.NewRecorder()
h2.ServeHTTP(rec, req)
if rec.Code == 200 {
t.Error("FAIL: a forge with no secrets answered the route")
}
t.Log("ok: only the claiming runner, only what the rule allows, never cached")
}