CI / changes (push) Successful in 15s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 1m0s
CI / go (push) Successful in 1m0s
CI / bird2 (push) Successful in 16s
CI / release (push) Successful in 3m39s
Added public HTTPS endpoints for firewall installation and enrollment scripts, allowing access without API keys. Updated the URL handling in the firewall code to ensure all suggested control plane URLs are served over HTTPS. Enhanced documentation to reflect the new public endpoints and their usage. Updated tests to verify the correct behavior of the new URL handling logic.
215 lines
6.6 KiB
Go
215 lines
6.6 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"evobgp/internal/authkey"
|
|
"evobgp/internal/store"
|
|
)
|
|
|
|
func TestFirewallEnrollAndBlocklist(t *testing.T) {
|
|
srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer srv.Close()
|
|
tenant, _, _, _, _ := srv.Store().DemoIDs()
|
|
mustSetTestAPIKeys(t, srv, "opkey|"+tenant+"|operator")
|
|
|
|
ts := httptest.NewServer(srv.Handler())
|
|
defer ts.Close()
|
|
client := ts.Client()
|
|
|
|
tok := "evobgp_fw_testtoken123456789012345678901234"
|
|
enrollBody := `{"name":"web-01","hostname":"web-01.local","client_token":"` + tok + `","client_version":"test/1"}`
|
|
reqEnroll, _ := http.NewRequest(http.MethodPost, ts.URL+"/v1/firewall/enroll", strings.NewReader(enrollBody))
|
|
reqEnroll.Header.Set("Content-Type", "application/json")
|
|
reqEnroll.Header.Set("X-EvoBGP-Seed", testBundleSeed)
|
|
respEnroll, err := client.Do(reqEnroll)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = respEnroll.Body.Close() }()
|
|
if respEnroll.StatusCode != http.StatusCreated {
|
|
b, _ := io.ReadAll(respEnroll.Body)
|
|
t.Fatalf("enroll status=%d body=%s", respEnroll.StatusCode, b)
|
|
}
|
|
var enroll map[string]any
|
|
if err := json.NewDecoder(respEnroll.Body).Decode(&enroll); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
clientID, _ := enroll["client_id"].(string)
|
|
if clientID == "" {
|
|
t.Fatal("missing client_id")
|
|
}
|
|
|
|
reqBlock, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/firewall/blocklist", nil)
|
|
reqBlock.Header.Set("Authorization", "Bearer "+tok)
|
|
respBlock, err := client.Do(reqBlock)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = respBlock.Body.Close() }()
|
|
if respBlock.StatusCode != http.StatusForbidden {
|
|
t.Fatalf("pending blocklist want 403 got %d", respBlock.StatusCode)
|
|
}
|
|
|
|
reqApprove, _ := http.NewRequest(http.MethodPost, ts.URL+"/v1/firewall/clients/"+clientID+"/approve", nil)
|
|
reqApprove.Header.Set("Authorization", "Bearer opkey")
|
|
respApprove, err := client.Do(reqApprove)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = respApprove.Body.Close() }()
|
|
if respApprove.StatusCode != http.StatusOK {
|
|
b, _ := io.ReadAll(respApprove.Body)
|
|
t.Fatalf("approve status=%d body=%s", respApprove.StatusCode, b)
|
|
}
|
|
|
|
_, err = srv.Store().CreateFirewallRule(tenant, nil, &store.FirewallRuleCreate{Action: "accept", Comment: "default"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
reqBlock2, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/firewall/blocklist", nil)
|
|
reqBlock2.Header.Set("Authorization", "Bearer "+tok)
|
|
respBlock2, err := client.Do(reqBlock2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = respBlock2.Body.Close() }()
|
|
if respBlock2.StatusCode != http.StatusOK {
|
|
b, _ := io.ReadAll(respBlock2.Body)
|
|
t.Fatalf("blocklist status=%d body=%s", respBlock2.StatusCode, b)
|
|
}
|
|
var bl map[string]any
|
|
if err := json.NewDecoder(respBlock2.Body).Decode(&bl); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if total, _ := bl["total"].(float64); total != 0 {
|
|
t.Fatalf("accept-only want empty blocklist, total=%v", total)
|
|
}
|
|
}
|
|
|
|
func TestFirewallEnrollBadSeed(t *testing.T) {
|
|
srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer srv.Close()
|
|
ts := httptest.NewServer(srv.Handler())
|
|
defer ts.Close()
|
|
|
|
body := `{"name":"x","client_token":"evobgp_fw_` + strings.Repeat("a", 40) + `"}`
|
|
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/v1/firewall/enroll", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-EvoBGP-Seed", "deadbeef")
|
|
resp, err := ts.Client().Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode != http.StatusForbidden {
|
|
t.Fatalf("want 403 got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestFirewallInstallScriptPublic(t *testing.T) {
|
|
srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer srv.Close()
|
|
ts := httptest.NewServer(srv.Handler())
|
|
defer ts.Close()
|
|
|
|
for _, path := range []string{"/v1/firewall/install.sh", "/v1/firewall/sync-script"} {
|
|
req, _ := http.NewRequest(http.MethodGet, ts.URL+path, nil)
|
|
resp, err := ts.Client().Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
func() {
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode != http.StatusOK {
|
|
b, _ := io.ReadAll(resp.Body)
|
|
t.Fatalf("%s status=%d body=%s", path, resp.StatusCode, b)
|
|
}
|
|
ct := resp.Header.Get("Content-Type")
|
|
if !strings.Contains(ct, "shellscript") {
|
|
t.Fatalf("%s content-type=%q", path, ct)
|
|
}
|
|
b, _ := io.ReadAll(resp.Body)
|
|
if !strings.HasPrefix(string(b), "#!/") {
|
|
t.Fatalf("%s missing shebang", path)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
func TestFirewallInstallContext(t *testing.T) {
|
|
srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer srv.Close()
|
|
tenant, _, _, _, _ := srv.Store().DemoIDs()
|
|
mustSetTestAPIKeys(t, srv, "opkey|"+tenant+"|operator,vwkey|"+tenant+"|viewer")
|
|
|
|
ts := httptest.NewServer(srv.Handler())
|
|
defer ts.Close()
|
|
client := ts.Client()
|
|
|
|
reqOp, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/firewall/install-context", nil)
|
|
reqOp.Header.Set("Authorization", "Bearer opkey")
|
|
respOp, err := client.Do(reqOp)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = respOp.Body.Close() }()
|
|
if respOp.StatusCode != http.StatusOK {
|
|
b, _ := io.ReadAll(respOp.Body)
|
|
t.Fatalf("operator install-context status=%d body=%s", respOp.StatusCode, b)
|
|
}
|
|
var ctx map[string]any
|
|
if err := json.NewDecoder(respOp.Body).Decode(&ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if seed, _ := ctx["bundle_seed"].(string); seed != testBundleSeed {
|
|
t.Fatalf("bundle_seed=%q want %q", seed, testBundleSeed)
|
|
}
|
|
if configured, _ := ctx["bundle_seed_configured"].(bool); !configured {
|
|
t.Fatal("bundle_seed_configured want true")
|
|
}
|
|
if url, _ := ctx["suggested_cp_url"].(string); !strings.HasPrefix(url, "https://") {
|
|
t.Fatalf("suggested_cp_url=%q want https", url)
|
|
}
|
|
if url, _ := ctx["install_sh_url"].(string); !strings.HasPrefix(url, "https://") || !strings.HasSuffix(url, "/v1/firewall/install.sh") {
|
|
t.Fatalf("install_sh_url=%q", url)
|
|
}
|
|
|
|
reqVw, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/firewall/install-context", nil)
|
|
reqVw.Header.Set("Authorization", "Bearer vwkey")
|
|
respVw, err := client.Do(reqVw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = respVw.Body.Close() }()
|
|
if respVw.StatusCode != http.StatusForbidden {
|
|
t.Fatalf("viewer install-context want 403 got %d", respVw.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestFirewallTokenHashMatchesAuthkey(t *testing.T) {
|
|
tok := "evobgp_fw_sample"
|
|
h := authkey.HashToken(tok)
|
|
if len(h) != 32 {
|
|
t.Fatalf("hash len %d", len(h))
|
|
}
|
|
}
|