From 7877f1eab652356e597321ab58a8ded95e135972 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Fri, 23 Oct 2026 21:34:07 +1100 Subject: [PATCH 1/3] feat: add set-testing subcommand to pin a node to testing --- README.md | 17 ++++++ main.go | 5 +- settesting.go | 140 +++++++++++++++++++++++++++++++++++++++++++++ settesting_test.go | 108 ++++++++++++++++++++++++++++++++++ 4 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 settesting.go create mode 100644 settesting_test.go diff --git a/README.md b/README.md index 11a2220..23022ea 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,23 @@ exec `node_terminus` fails safe rather than compiling an empty catalog. `http://encapi.encapi.svc.cluster.local` (the in-cluster service). - The HTTP request has a 10s timeout. +### Pinning a host to `testing` + +``` +encapic set-testing +``` + +Sets the node's environment to `testing` in encapi (requires the encapi +oidc-auth release). A `testing` node's ENC omits `environment`, so any +`puppet agent --environment X` is honoured; the host stays in `testing` until +the next terragrunt-enc apply resets it. + +It mints a JWT from `${VAULT_ADDR}/v1/identity/oidc/token/${ENCAPI_OIDC_ROLE}` +(defaults `https://vault.service.consul:8200`, `encapi-testing`) using +`VAULT_TOKEN` or `~/.vault-token`, trusting `VAULT_CACERT` when set, then +`PUT`s `{"environment":"testing"}` to +`${ENCAPI_URL}/api/v1/nodes//environment`. + ## Behaviour (drop-in for the python ENC) encapic reproduces the previous python script byte-for-byte: diff --git a/main.go b/main.go index c053f17..80c02c7 100644 --- a/main.go +++ b/main.go @@ -58,8 +58,11 @@ func run(args []string, out io.Writer) error { _, err := fmt.Fprintf(out, "encapic %s\n", version) return err } + if len(args) == 3 && args[1] == "set-testing" { + return setTesting(args[2], out) + } if len(args) != 2 { - return fmt.Errorf("usage: %s ", args[0]) + return fmt.Errorf("usage: %s | %s set-testing ", args[0], args[0]) } certname := args[1] diff --git a/settesting.go b/settesting.go new file mode 100644 index 0000000..c5f5e90 --- /dev/null +++ b/settesting.go @@ -0,0 +1,140 @@ +package main + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" +) + +const defaultVaultAddr = "https://vault.service.consul:8200" + +// setTesting pins certname to the testing environment via encapi, using a +// Vault identity OIDC token for the encapi-testing role. +func setTesting(certname string, out io.Writer) error { + client, err := vaultClient() + if err != nil { + return err + } + jwt, err := mintToken(client) + if err != nil { + return err + } + + baseURL := os.Getenv("ENCAPI_URL") + if baseURL == "" { + baseURL = defaultBaseURL + } + u := strings.TrimRight(baseURL, "/") + "/api/v1/nodes/" + url.PathEscape(certname) + "/environment" + req, err := http.NewRequest(http.MethodPut, u, bytes.NewBufferString(`{"environment":"testing"}`)) + if err != nil { + return err + } + req.Header.Set("Authorization", "Bearer "+jwt) + req.Header.Set("Content-Type", "application/json") + if _, err := do(client, req); err != nil { + return err + } + _, err = fmt.Fprintf(out, "%s set to testing\n", certname) + return err +} + +func mintToken(client *http.Client) (string, error) { + addr := os.Getenv("VAULT_ADDR") + if addr == "" { + addr = defaultVaultAddr + } + role := os.Getenv("ENCAPI_OIDC_ROLE") + if role == "" { + role = "encapi-testing" + } + vaultToken, err := readVaultToken() + if err != nil { + return "", err + } + req, err := http.NewRequest(http.MethodGet, strings.TrimRight(addr, "/")+"/v1/identity/oidc/token/"+url.PathEscape(role), nil) + if err != nil { + return "", err + } + req.Header.Set("X-Vault-Token", vaultToken) + body, err := do(client, req) + if err != nil { + return "", err + } + var resp struct { + Data struct { + Token string `json:"token"` + } `json:"data"` + } + if err := json.Unmarshal(body, &resp); err != nil { + return "", fmt.Errorf("decode vault token response: %w", err) + } + if resp.Data.Token == "" { + return "", errors.New("vault returned no OIDC token") + } + return resp.Data.Token, nil +} + +func readVaultToken() (string, error) { + if t := os.Getenv("VAULT_TOKEN"); t != "" { + return t, nil + } + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + b, err := os.ReadFile(filepath.Join(home, ".vault-token")) + if err != nil { + return "", fmt.Errorf("no VAULT_TOKEN and cannot read ~/.vault-token: %w", err) + } + return strings.TrimSpace(string(b)), nil +} + +func vaultClient() (*http.Client, error) { + client := &http.Client{Timeout: httpTimeout} + caFile := os.Getenv("VAULT_CACERT") + if caFile == "" { + return client, nil + } + pem, err := os.ReadFile(caFile) + if err != nil { + return nil, fmt.Errorf("read VAULT_CACERT: %w", err) + } + pool, err := x509.SystemCertPool() + if err != nil { + pool = x509.NewCertPool() + } + if !pool.AppendCertsFromPEM(pem) { + return nil, fmt.Errorf("no certificates in VAULT_CACERT %s", caFile) + } + client.Transport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12}, + } + return client, nil +} + +func do(client *http.Client, req *http.Request) ([]byte, error) { + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("%s %s: %w", req.Method, req.URL, err) + } + defer func() { _ = resp.Body.Close() }() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("read response from %s: %w", req.URL, err) + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("%s %s returned HTTP %d: %s", + req.Method, req.URL, resp.StatusCode, strings.TrimSpace(string(body))) + } + return body, nil +} diff --git a/settesting_test.go b/settesting_test.go new file mode 100644 index 0000000..7583a84 --- /dev/null +++ b/settesting_test.go @@ -0,0 +1,108 @@ +package main + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" +) + +func fakeVault(t *testing.T, status int, body string) *httptest.Server { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Path != "/v1/identity/oidc/token/encapi-testing" { + http.NotFound(w, r) + return + } + if r.Header.Get("X-Vault-Token") != "vt" { + http.Error(w, `{"errors":["permission denied"]}`, http.StatusForbidden) + return + } + w.WriteHeader(status) + _, _ = io.WriteString(w, body) + })) + t.Cleanup(srv.Close) + return srv +} + +type seenPut struct{ path, auth, body string } + +func fakeEncapi(t *testing.T, status int, seen *seenPut) *httptest.Server { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, _ := io.ReadAll(r.Body) + if r.Method == http.MethodPut { + *seen = seenPut{r.URL.Path, r.Header.Get("Authorization"), string(b)} + } + w.WriteHeader(status) + _, _ = io.WriteString(w, `{"error":"forbidden"}`) + })) + t.Cleanup(srv.Close) + return srv +} + +func TestSetTestingSuccess(t *testing.T) { + var seen seenPut + t.Setenv("VAULT_ADDR", fakeVault(t, 200, `{"data":{"token":"jwt123"}}`).URL) + t.Setenv("VAULT_TOKEN", "vt") + t.Setenv("ENCAPI_URL", fakeEncapi(t, 200, &seen).URL) + + var out bytes.Buffer + if err := run([]string{"encapic", "set-testing", "host.example"}, &out); err != nil { + t.Fatalf("run: %v", err) + } + want := seenPut{"/api/v1/nodes/host.example/environment", "Bearer jwt123", `{"environment":"testing"}`} + if seen != want { + t.Errorf("PUT = %+v, want %+v", seen, want) + } +} + +func TestSetTestingVaultTokenFile(t *testing.T) { + var seen seenPut + home := t.TempDir() + if err := os.WriteFile(filepath.Join(home, ".vault-token"), []byte("vt\n"), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("HOME", home) + t.Setenv("VAULT_TOKEN", "") + t.Setenv("VAULT_ADDR", fakeVault(t, 200, `{"data":{"token":"jwt123"}}`).URL) + t.Setenv("ENCAPI_URL", fakeEncapi(t, 204, &seen).URL) + + if err := run([]string{"encapic", "set-testing", "host.example"}, io.Discard); err != nil { + t.Fatalf("run: %v", err) + } + if seen.auth != "Bearer jwt123" { + t.Errorf("auth = %q", seen.auth) + } +} + +func TestSetTestingVaultError(t *testing.T) { + var seen seenPut + t.Setenv("VAULT_ADDR", fakeVault(t, 400, `{"errors":["role not found"]}`).URL) + t.Setenv("VAULT_TOKEN", "vt") + t.Setenv("ENCAPI_URL", fakeEncapi(t, 200, &seen).URL) + + err := run([]string{"encapic", "set-testing", "host.example"}, io.Discard) + if err == nil || !strings.Contains(err.Error(), "HTTP 400") || !strings.Contains(err.Error(), "role not found") { + t.Fatalf("err = %v, want HTTP 400 with body", err) + } + if seen.path != "" { + t.Errorf("encapi called despite vault failure: %+v", seen) + } +} + +func TestSetTestingEncapiForbidden(t *testing.T) { + var seen seenPut + t.Setenv("VAULT_ADDR", fakeVault(t, 200, `{"data":{"token":"jwt123"}}`).URL) + t.Setenv("VAULT_TOKEN", "vt") + t.Setenv("ENCAPI_URL", fakeEncapi(t, 403, &seen).URL) + + err := run([]string{"encapic", "set-testing", "host.example"}, io.Discard) + if err == nil || !strings.Contains(err.Error(), "HTTP 403") || !strings.Contains(err.Error(), "forbidden") { + t.Fatalf("err = %v, want HTTP 403 with body", err) + } +} -- 2.47.3 From df0221bcbf57fd715f40a21f7c16badcda0cab35 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Fri, 23 Oct 2026 21:36:38 +1100 Subject: [PATCH 2/3] fix: refuse non-https encapi URL in set-testing; cover vault CA and token errors --- README.md | 3 +- settesting.go | 11 +++-- settesting_test.go | 110 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 118 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 23022ea..d5b00c1 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,8 @@ It mints a JWT from `${VAULT_ADDR}/v1/identity/oidc/token/${ENCAPI_OIDC_ROLE}` (defaults `https://vault.service.consul:8200`, `encapi-testing`) using `VAULT_TOKEN` or `~/.vault-token`, trusting `VAULT_CACERT` when set, then `PUT`s `{"environment":"testing"}` to -`${ENCAPI_URL}/api/v1/nodes//environment`. +`${ENCAPI_URL}/api/v1/nodes//environment`. `ENCAPI_URL` must be +`https`; the bearer JWT is never sent over plain http. ## Behaviour (drop-in for the python ENC) diff --git a/settesting.go b/settesting.go index c5f5e90..8673526 100644 --- a/settesting.go +++ b/settesting.go @@ -20,6 +20,13 @@ const defaultVaultAddr = "https://vault.service.consul:8200" // setTesting pins certname to the testing environment via encapi, using a // Vault identity OIDC token for the encapi-testing role. func setTesting(certname string, out io.Writer) error { + baseURL := os.Getenv("ENCAPI_URL") + if baseURL == "" { + baseURL = defaultBaseURL + } + if parsed, err := url.Parse(baseURL); err != nil || parsed.Scheme != "https" { + return fmt.Errorf("set-testing requires an https ENCAPI_URL, got %q", baseURL) + } client, err := vaultClient() if err != nil { return err @@ -29,10 +36,6 @@ func setTesting(certname string, out io.Writer) error { return err } - baseURL := os.Getenv("ENCAPI_URL") - if baseURL == "" { - baseURL = defaultBaseURL - } u := strings.TrimRight(baseURL, "/") + "/api/v1/nodes/" + url.PathEscape(certname) + "/environment" req, err := http.NewRequest(http.MethodPut, u, bytes.NewBufferString(`{"environment":"testing"}`)) if err != nil { diff --git a/settesting_test.go b/settesting_test.go index 7583a84..6bd4905 100644 --- a/settesting_test.go +++ b/settesting_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "encoding/pem" "io" "net/http" "net/http/httptest" @@ -33,7 +34,7 @@ type seenPut struct{ path, auth, body string } func fakeEncapi(t *testing.T, status int, seen *seenPut) *httptest.Server { t.Helper() - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, _ := io.ReadAll(r.Body) if r.Method == http.MethodPut { *seen = seenPut{r.URL.Path, r.Header.Get("Authorization"), string(b)} @@ -42,9 +43,21 @@ func fakeEncapi(t *testing.T, status int, seen *seenPut) *httptest.Server { _, _ = io.WriteString(w, `{"error":"forbidden"}`) })) t.Cleanup(srv.Close) + trustServer(t, srv) return srv } +// trustServer points VAULT_CACERT at the TLS server's certificate. +func trustServer(t *testing.T, srv *httptest.Server) { + t.Helper() + path := filepath.Join(t.TempDir(), "ca.pem") + b := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: srv.Certificate().Raw}) + if err := os.WriteFile(path, b, 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("VAULT_CACERT", path) +} + func TestSetTestingSuccess(t *testing.T) { var seen seenPut t.Setenv("VAULT_ADDR", fakeVault(t, 200, `{"data":{"token":"jwt123"}}`).URL) @@ -106,3 +119,98 @@ func TestSetTestingEncapiForbidden(t *testing.T) { t.Fatalf("err = %v, want HTTP 403 with body", err) } } + +func TestSetTestingRefusesPlainHTTP(t *testing.T) { + vaultHit := false + vault := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { vaultHit = true })) + t.Cleanup(vault.Close) + t.Setenv("VAULT_ADDR", vault.URL) + t.Setenv("VAULT_TOKEN", "vt") + + for _, encapiURL := range []string{"http://encapi.example", "", "encapi.example", "://bad"} { + t.Setenv("ENCAPI_URL", encapiURL) + err := run([]string{"encapic", "set-testing", "host.example"}, io.Discard) + if err == nil || !strings.Contains(err.Error(), "requires an https ENCAPI_URL") { + t.Errorf("ENCAPI_URL=%q: err = %v, want https refusal", encapiURL, err) + } + } + if vaultHit { + t.Error("vault called despite non-https ENCAPI_URL") + } +} + +func TestSetTestingVaultNoToken(t *testing.T) { + for _, body := range []string{`{"data":{"token":""}}`, `{"data":{}}`, `{}`} { + var seen seenPut + t.Setenv("VAULT_ADDR", fakeVault(t, 200, body).URL) + t.Setenv("VAULT_TOKEN", "vt") + t.Setenv("ENCAPI_URL", fakeEncapi(t, 200, &seen).URL) + + err := run([]string{"encapic", "set-testing", "host.example"}, io.Discard) + if err == nil || !strings.Contains(err.Error(), "no OIDC token") { + t.Errorf("body %s: err = %v, want no OIDC token", body, err) + } + if seen.path != "" { + t.Errorf("body %s: encapi called: %+v", body, seen) + } + } +} + +func TestSetTestingNoVaultToken(t *testing.T) { + var seen seenPut + t.Setenv("HOME", t.TempDir()) + t.Setenv("VAULT_TOKEN", "") + t.Setenv("VAULT_ADDR", fakeVault(t, 200, `{"data":{"token":"jwt123"}}`).URL) + t.Setenv("ENCAPI_URL", fakeEncapi(t, 200, &seen).URL) + + err := run([]string{"encapic", "set-testing", "host.example"}, io.Discard) + if err == nil || !strings.Contains(err.Error(), "no VAULT_TOKEN and cannot read ~/.vault-token") { + t.Fatalf("err = %v, want missing vault token", err) + } + if seen.path != "" { + t.Errorf("encapi called: %+v", seen) + } +} + +func TestVaultClientCACertUnreadable(t *testing.T) { + t.Setenv("VAULT_CACERT", filepath.Join(t.TempDir(), "missing.pem")) + if _, err := vaultClient(); err == nil || !strings.Contains(err.Error(), "read VAULT_CACERT") { + t.Fatalf("err = %v, want read VAULT_CACERT", err) + } +} + +func TestVaultClientCACertNoPEM(t *testing.T) { + path := filepath.Join(t.TempDir(), "ca.pem") + if err := os.WriteFile(path, []byte("not a certificate\n"), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("VAULT_CACERT", path) + if _, err := vaultClient(); err == nil || !strings.Contains(err.Error(), "no certificates in VAULT_CACERT") { + t.Fatalf("err = %v, want no certificates", err) + } +} + +func TestVaultClientCACertTrustsServer(t *testing.T) { + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + t.Cleanup(srv.Close) + + t.Setenv("VAULT_CACERT", "") + plain, err := vaultClient() + if err != nil { + t.Fatal(err) + } + if _, err := plain.Get(srv.URL); err == nil { + t.Fatal("untrusted TLS server accepted without VAULT_CACERT") + } + + trustServer(t, srv) + client, err := vaultClient() + if err != nil { + t.Fatalf("vaultClient: %v", err) + } + resp, err := client.Get(srv.URL) + if err != nil { + t.Fatalf("GET with VAULT_CACERT: %v", err) + } + _ = resp.Body.Close() +} -- 2.47.3 From b664d2f12f5185eb39003674644365683057bdc3 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Fri, 23 Oct 2026 21:38:43 +1100 Subject: [PATCH 3/3] fix: require https VAULT_ADDR and never follow redirects in set-testing --- README.md | 5 +++-- settesting.go | 20 +++++++++++++++++--- settesting_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d5b00c1..a4d108c 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,9 @@ It mints a JWT from `${VAULT_ADDR}/v1/identity/oidc/token/${ENCAPI_OIDC_ROLE}` (defaults `https://vault.service.consul:8200`, `encapi-testing`) using `VAULT_TOKEN` or `~/.vault-token`, trusting `VAULT_CACERT` when set, then `PUT`s `{"environment":"testing"}` to -`${ENCAPI_URL}/api/v1/nodes//environment`. `ENCAPI_URL` must be -`https`; the bearer JWT is never sent over plain http. +`${ENCAPI_URL}/api/v1/nodes//environment`. `ENCAPI_URL` and +`VAULT_ADDR` must be `https` and redirects are never followed, so neither +the Vault token nor the JWT is sent over plain http or to another host. ## Behaviour (drop-in for the python ENC) diff --git a/settesting.go b/settesting.go index 8673526..46218bd 100644 --- a/settesting.go +++ b/settesting.go @@ -24,8 +24,8 @@ func setTesting(certname string, out io.Writer) error { if baseURL == "" { baseURL = defaultBaseURL } - if parsed, err := url.Parse(baseURL); err != nil || parsed.Scheme != "https" { - return fmt.Errorf("set-testing requires an https ENCAPI_URL, got %q", baseURL) + if err := requireHTTPS("ENCAPI_URL", baseURL); err != nil { + return err } client, err := vaultClient() if err != nil { @@ -55,6 +55,9 @@ func mintToken(client *http.Client) (string, error) { if addr == "" { addr = defaultVaultAddr } + if err := requireHTTPS("VAULT_ADDR", addr); err != nil { + return "", err + } role := os.Getenv("ENCAPI_OIDC_ROLE") if role == "" { role = "encapi-testing" @@ -86,6 +89,13 @@ func mintToken(client *http.Client) (string, error) { return resp.Data.Token, nil } +func requireHTTPS(name, raw string) error { + if parsed, err := url.Parse(raw); err != nil || parsed.Scheme != "https" { + return fmt.Errorf("set-testing requires an https %s, got %q", name, raw) + } + return nil +} + func readVaultToken() (string, error) { if t := os.Getenv("VAULT_TOKEN"); t != "" { return t, nil @@ -102,7 +112,11 @@ func readVaultToken() (string, error) { } func vaultClient() (*http.Client, error) { - client := &http.Client{Timeout: httpTimeout} + client := &http.Client{ + Timeout: httpTimeout, + // Never follow redirects: they would re-send the vault token or JWT. + CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }, + } caFile := os.Getenv("VAULT_CACERT") if caFile == "" { return client, nil diff --git a/settesting_test.go b/settesting_test.go index 6bd4905..9121e16 100644 --- a/settesting_test.go +++ b/settesting_test.go @@ -14,7 +14,7 @@ import ( func fakeVault(t *testing.T, status int, body string) *httptest.Server { t.Helper() - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet || r.URL.Path != "/v1/identity/oidc/token/encapi-testing" { http.NotFound(w, r) return @@ -27,6 +27,7 @@ func fakeVault(t *testing.T, status int, body string) *httptest.Server { _, _ = io.WriteString(w, body) })) t.Cleanup(srv.Close) + trustServer(t, srv) return srv } @@ -47,7 +48,8 @@ func fakeEncapi(t *testing.T, status int, seen *seenPut) *httptest.Server { return srv } -// trustServer points VAULT_CACERT at the TLS server's certificate. +// trustServer points VAULT_CACERT at the TLS server's certificate; all +// httptest TLS servers share one certificate, so this trusts both fakes. func trustServer(t *testing.T, srv *httptest.Server) { t.Helper() path := filepath.Join(t.TempDir(), "ca.pem") @@ -139,6 +141,46 @@ func TestSetTestingRefusesPlainHTTP(t *testing.T) { } } +func TestSetTestingRefusesPlainHTTPVault(t *testing.T) { + vaultHit := false + vault := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { vaultHit = true })) + t.Cleanup(vault.Close) + var seen seenPut + t.Setenv("ENCAPI_URL", fakeEncapi(t, 200, &seen).URL) + t.Setenv("VAULT_TOKEN", "vt") + + for _, addr := range []string{vault.URL, "vault.example:8200", "://bad"} { + t.Setenv("VAULT_ADDR", addr) + err := run([]string{"encapic", "set-testing", "host.example"}, io.Discard) + if err == nil || !strings.Contains(err.Error(), "requires an https VAULT_ADDR") { + t.Errorf("VAULT_ADDR=%q: err = %v, want https refusal", addr, err) + } + } + if vaultHit || seen.path != "" { + t.Errorf("request sent despite non-https VAULT_ADDR: vault=%v encapi=%+v", vaultHit, seen) + } +} + +func TestSetTestingDoesNotFollowRedirect(t *testing.T) { + var hits []string + encapi := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hits = append(hits, r.URL.Path+" "+r.Header.Get("Authorization")) + http.Redirect(w, r, "/elsewhere", http.StatusTemporaryRedirect) + })) + t.Cleanup(encapi.Close) + t.Setenv("VAULT_ADDR", fakeVault(t, 200, `{"data":{"token":"jwt123"}}`).URL) + t.Setenv("VAULT_TOKEN", "vt") + t.Setenv("ENCAPI_URL", encapi.URL) + + err := run([]string{"encapic", "set-testing", "host.example"}, io.Discard) + if err == nil || !strings.Contains(err.Error(), "HTTP 307") { + t.Fatalf("err = %v, want HTTP 307", err) + } + if len(hits) != 1 { + t.Errorf("encapi requests = %v, want exactly one", hits) + } +} + func TestSetTestingVaultNoToken(t *testing.T) { for _, body := range []string{`{"data":{"token":""}}`, `{"data":{}}`, `{}`} { var seen seenPut -- 2.47.3