Skip to content
quarry Artifact [e8a7b7ab8f]
Login

Artifact [e8a7b7ab8f]

Artifact e8a7b7ab8f807e3ff97ffa85a476f4f0c2f1244a8846a96e188bbabb53231d32:


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