Two safe-by-default flips, both opt-out via explicit acknowledgement. 1. --insecure / ZDDC_INSECURE=1: zddc-server now refuses to start when no <ZDDC_ROOT>/.zddc exists. With no .zddc anywhere in the chain, AllowedWithChain falls through to "HasAnyFile=false → allow" and the tree is publicly accessible to anonymous callers — almost never what an operator wants on a fresh deployment, and previously a silent footgun. The flag is the escape hatch for deliberately- public archives (no .zddc anywhere by design). 2. ZDDC_CORS_ORIGIN now defaults to empty (CORS disabled) instead of the canonical "https://zddc.varasys.io". The embedded-tools install path serves tools and data same-origin, so the default never needed to permit cross-origin XHRs from a third-party host. Every deployment was implicitly trusting zddc.varasys.io to make authenticated XHRs on behalf of every logged-in user; if that origin were ever compromised, the blast radius extended to every customer server. Operators who deliberately use the CDN-bootstrap pattern or self- hosted tools at a different host now set the value explicitly. Helm chart values updated accordingly: prod default is empty; dev keeps localhost:8000 for tool-iteration workflows. Existing deployments that depended on the old defaults will need to either set the value explicitly or pass --insecure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
392 lines
12 KiB
Go
392 lines
12 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestIsLoopbackAddr(t *testing.T) {
|
|
cases := []struct {
|
|
addr string
|
|
want bool
|
|
}{
|
|
{"127.0.0.1:8080", true},
|
|
{"localhost:8080", true},
|
|
{"[::1]:8080", true},
|
|
{"127.0.0.5:80", true}, // any 127/8 is loopback
|
|
{"0.0.0.0:8080", false},
|
|
{":8443", false}, // bare port = all interfaces
|
|
{"10.0.0.1:80", false},
|
|
{"example.com:443", false},
|
|
{"[2001:db8::1]:80", false},
|
|
{"", false},
|
|
{"not-an-addr", false}, // SplitHostPort fails
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.addr, func(t *testing.T) {
|
|
if got := isLoopbackAddr(tc.addr); got != tc.want {
|
|
t.Errorf("isLoopbackAddr(%q) = %v, want %v", tc.addr, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoad(t *testing.T) {
|
|
root := t.TempDir()
|
|
// Drop a placeholder .zddc so subtests using this root don't trip the
|
|
// "no root .zddc → refuse to start" safety check. Tests that explicitly
|
|
// exercise the missing-.zddc path use a dedicated tmpdir without one.
|
|
if err := os.WriteFile(filepath.Join(root, ".zddc"), []byte("admins: [test@example.com]\n"), 0o644); err != nil {
|
|
t.Fatalf("seed root .zddc: %v", err)
|
|
}
|
|
|
|
// Pre-set the env so each subtest can override what it needs.
|
|
type envSet map[string]string
|
|
clearAll := func() {
|
|
for _, k := range []string{"ZDDC_ROOT", "ZDDC_ADDR", "ZDDC_TLS_CERT", "ZDDC_TLS_KEY", "ZDDC_INSECURE_DIRECT", "ZDDC_INSECURE", "ZDDC_LOG_LEVEL", "ZDDC_INDEX_PATH", "ZDDC_EMAIL_HEADER", "ZDDC_CORS_ORIGIN"} {
|
|
os.Unsetenv(k)
|
|
}
|
|
}
|
|
apply := func(env envSet) {
|
|
clearAll()
|
|
for k, v := range env {
|
|
os.Setenv(k, v)
|
|
}
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
env envSet
|
|
wantErr bool
|
|
errContains string
|
|
check func(*testing.T, Config)
|
|
}{
|
|
{
|
|
name: "missing root defaults to CWD",
|
|
// ZDDC_INSECURE=1 because the package's CWD has no .zddc; this test
|
|
// is specifically about Root resolution falling back to CWD, not
|
|
// about the .zddc safety check.
|
|
env: envSet{"ZDDC_INSECURE": "1"},
|
|
// ZDDC_ROOT not set → Load falls back to os.Getwd().
|
|
check: func(t *testing.T, cfg Config) {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("Getwd: %v", err)
|
|
}
|
|
// os.Stat resolves symlinks; so does Load via filepath behavior, so
|
|
// just compare the resolved values.
|
|
if cfg.Root != cwd {
|
|
t.Errorf("Root = %q, want CWD %q", cfg.Root, cwd)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "root not a directory",
|
|
env: envSet{"ZDDC_ROOT": filepath.Join(root, "does-not-exist")},
|
|
wantErr: true,
|
|
errContains: "not accessible",
|
|
},
|
|
{
|
|
name: "defaults applied with TLS self-signed",
|
|
env: envSet{"ZDDC_ROOT": root},
|
|
check: func(t *testing.T, cfg Config) {
|
|
if cfg.Addr != ":8443" {
|
|
t.Errorf("Addr = %q, want :8443", cfg.Addr)
|
|
}
|
|
if cfg.TLSMode != "selfsigned" {
|
|
t.Errorf("TLSMode = %q, want selfsigned", cfg.TLSMode)
|
|
}
|
|
if cfg.IndexPath != ".archive" {
|
|
t.Errorf("IndexPath = %q, want .archive", cfg.IndexPath)
|
|
}
|
|
if cfg.EmailHeader != "X-Auth-Request-Email" {
|
|
t.Errorf("EmailHeader = %q, want X-Auth-Request-Email", cfg.EmailHeader)
|
|
}
|
|
if len(cfg.CORSOrigins) != 0 {
|
|
t.Errorf("CORSOrigins = %v, want empty (CORS disabled by default)", cfg.CORSOrigins)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "CORS single origin override",
|
|
env: envSet{
|
|
"ZDDC_ROOT": root,
|
|
"ZDDC_CORS_ORIGIN": "https://tools.acme.com",
|
|
},
|
|
check: func(t *testing.T, cfg Config) {
|
|
if len(cfg.CORSOrigins) != 1 || cfg.CORSOrigins[0] != "https://tools.acme.com" {
|
|
t.Errorf("CORSOrigins = %v, want [https://tools.acme.com]", cfg.CORSOrigins)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "CORS multi-origin allowlist",
|
|
env: envSet{
|
|
"ZDDC_ROOT": root,
|
|
"ZDDC_CORS_ORIGIN": "https://a.example, https://b.example ,https://c.example",
|
|
},
|
|
check: func(t *testing.T, cfg Config) {
|
|
want := []string{"https://a.example", "https://b.example", "https://c.example"}
|
|
if len(cfg.CORSOrigins) != len(want) {
|
|
t.Fatalf("CORSOrigins = %v, want %v", cfg.CORSOrigins, want)
|
|
}
|
|
for i, w := range want {
|
|
if cfg.CORSOrigins[i] != w {
|
|
t.Errorf("CORSOrigins[%d] = %q, want %q", i, cfg.CORSOrigins[i], w)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "CORS disabled with empty value",
|
|
env: envSet{
|
|
"ZDDC_ROOT": root,
|
|
"ZDDC_CORS_ORIGIN": "",
|
|
},
|
|
check: func(t *testing.T, cfg Config) {
|
|
if len(cfg.CORSOrigins) != 0 {
|
|
t.Errorf("CORSOrigins = %v, want empty (CORS disabled)", cfg.CORSOrigins)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "TLS provided requires both cert and key",
|
|
env: envSet{
|
|
"ZDDC_ROOT": root,
|
|
"ZDDC_TLS_CERT": "/some/cert.pem",
|
|
// missing key
|
|
},
|
|
wantErr: true,
|
|
errContains: "both be set or both be empty",
|
|
},
|
|
{
|
|
name: "plain HTTP on all interfaces without insecure flag is rejected",
|
|
env: envSet{
|
|
"ZDDC_ROOT": root,
|
|
"ZDDC_TLS_CERT": "none",
|
|
"ZDDC_ADDR": ":8080",
|
|
},
|
|
wantErr: true,
|
|
errContains: "--insecure-direct",
|
|
},
|
|
{
|
|
name: "plain HTTP on 0.0.0.0 without insecure flag is rejected",
|
|
env: envSet{
|
|
"ZDDC_ROOT": root,
|
|
"ZDDC_TLS_CERT": "none",
|
|
"ZDDC_ADDR": "0.0.0.0:8080",
|
|
},
|
|
wantErr: true,
|
|
errContains: "--insecure-direct",
|
|
},
|
|
{
|
|
name: "plain HTTP on loopback is allowed",
|
|
env: envSet{
|
|
"ZDDC_ROOT": root,
|
|
"ZDDC_TLS_CERT": "none",
|
|
"ZDDC_ADDR": "127.0.0.1:8080",
|
|
},
|
|
check: func(t *testing.T, cfg Config) {
|
|
if cfg.TLSMode != "none" {
|
|
t.Errorf("TLSMode = %q, want none", cfg.TLSMode)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "plain HTTP on non-loopback with explicit opt-in is allowed",
|
|
env: envSet{
|
|
"ZDDC_ROOT": root,
|
|
"ZDDC_TLS_CERT": "none",
|
|
"ZDDC_ADDR": ":8080",
|
|
"ZDDC_INSECURE_DIRECT": "1",
|
|
},
|
|
check: func(t *testing.T, cfg Config) {
|
|
if cfg.TLSMode != "none" {
|
|
t.Errorf("TLSMode = %q, want none", cfg.TLSMode)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "ZDDC_INSECURE_DIRECT set to non-1 is not opt-in",
|
|
env: envSet{
|
|
"ZDDC_ROOT": root,
|
|
"ZDDC_TLS_CERT": "none",
|
|
"ZDDC_ADDR": ":8080",
|
|
"ZDDC_INSECURE_DIRECT": "true", // must be exactly "1"
|
|
},
|
|
wantErr: true,
|
|
errContains: "--insecure-direct",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
apply(tc.env)
|
|
defer clearAll()
|
|
|
|
cfg, err := Load([]string{})
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("Load() = nil error, want error containing %q", tc.errContains)
|
|
}
|
|
if tc.errContains != "" && !strings.Contains(err.Error(), tc.errContains) {
|
|
t.Errorf("Load() error = %v, want substring %q", err, tc.errContains)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("Load() unexpected error: %v", err)
|
|
}
|
|
if tc.check != nil {
|
|
tc.check(t, cfg)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// tmpRootWithZddc creates a temp dir and seeds a minimal .zddc so the
|
|
// post-Root-stat safety check (refuse to start with no root .zddc) does
|
|
// not fire. Tests that exercise the safety check explicitly use
|
|
// t.TempDir() directly without seeding.
|
|
func tmpRootWithZddc(t *testing.T) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, ".zddc"), []byte("admins: [test@example.com]\n"), 0o644); err != nil {
|
|
t.Fatalf("seed root .zddc: %v", err)
|
|
}
|
|
return dir
|
|
}
|
|
|
|
// TestLoadFlags_OverrideEnv: --root flag wins over ZDDC_ROOT env var.
|
|
func TestLoadFlags_OverrideEnv(t *testing.T) {
|
|
envRoot := tmpRootWithZddc(t)
|
|
flagRoot := tmpRootWithZddc(t)
|
|
os.Setenv("ZDDC_ROOT", envRoot)
|
|
defer os.Unsetenv("ZDDC_ROOT")
|
|
|
|
cfg, err := Load([]string{"--root", flagRoot})
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if cfg.Root != flagRoot {
|
|
t.Errorf("Root = %q, want flag value %q", cfg.Root, flagRoot)
|
|
}
|
|
}
|
|
|
|
// TestLoadFlags_AddrLogLevelFromFlags: arbitrary flags override env defaults.
|
|
func TestLoadFlags_AddrLogLevelFromFlags(t *testing.T) {
|
|
root := tmpRootWithZddc(t)
|
|
cfg, err := Load([]string{
|
|
"--root", root,
|
|
"--addr", "127.0.0.1:9999",
|
|
"--log-level", "debug",
|
|
"--index-path", ".myindex",
|
|
"--email-header", "X-User-Email",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if cfg.Addr != "127.0.0.1:9999" {
|
|
t.Errorf("Addr=%q", cfg.Addr)
|
|
}
|
|
if cfg.LogLevel != "debug" {
|
|
t.Errorf("LogLevel=%q", cfg.LogLevel)
|
|
}
|
|
if cfg.IndexPath != ".myindex" {
|
|
t.Errorf("IndexPath=%q", cfg.IndexPath)
|
|
}
|
|
if cfg.EmailHeader != "X-User-Email" {
|
|
t.Errorf("EmailHeader=%q", cfg.EmailHeader)
|
|
}
|
|
}
|
|
|
|
// TestLoadFlags_CORSExplicitEmptyDisables: --cors-origin="" explicitly disables CORS.
|
|
func TestLoadFlags_CORSExplicitEmptyDisables(t *testing.T) {
|
|
root := tmpRootWithZddc(t)
|
|
cfg, err := Load([]string{"--root", root, "--cors-origin", ""})
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if len(cfg.CORSOrigins) != 0 {
|
|
t.Errorf("CORSOrigins = %v, want empty (CORS disabled by explicit empty flag)", cfg.CORSOrigins)
|
|
}
|
|
}
|
|
|
|
// TestLoadFlags_HelpRequested: --help returns the sentinel error.
|
|
func TestLoadFlags_HelpRequested(t *testing.T) {
|
|
_, err := Load([]string{"--help"})
|
|
if !strings.Contains(err.Error(), "help requested") && err != ErrHelpRequested {
|
|
t.Errorf("got err=%v, want ErrHelpRequested", err)
|
|
}
|
|
}
|
|
|
|
// TestLoadFlags_VersionRequested: --version returns the sentinel error.
|
|
func TestLoadFlags_VersionRequested(t *testing.T) {
|
|
_, err := Load([]string{"--version"})
|
|
if !strings.Contains(err.Error(), "version requested") && err != ErrVersionRequested {
|
|
t.Errorf("got err=%v, want ErrVersionRequested", err)
|
|
}
|
|
}
|
|
|
|
// TestLoadFlags_RootFlagDefaultsToCWD: with no --root and no ZDDC_ROOT, falls back to CWD.
|
|
func TestLoadFlags_RootFlagDefaultsToCWD(t *testing.T) {
|
|
os.Unsetenv("ZDDC_ROOT")
|
|
// ZDDC_INSECURE=1 because the package's CWD has no .zddc; this test is
|
|
// specifically about the CWD fallback, not the .zddc safety check.
|
|
os.Setenv("ZDDC_INSECURE", "1")
|
|
defer os.Unsetenv("ZDDC_INSECURE")
|
|
cfg, err := Load([]string{})
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
cwd, _ := os.Getwd()
|
|
if cfg.Root != cwd {
|
|
t.Errorf("Root=%q, want CWD=%q", cfg.Root, cwd)
|
|
}
|
|
}
|
|
|
|
// TestLoad_MissingRootZddcRefusesStartByDefault: with no .zddc at root and no
|
|
// --insecure, Load refuses to start (the public-by-default footgun).
|
|
func TestLoad_MissingRootZddcRefusesStartByDefault(t *testing.T) {
|
|
root := t.TempDir() // no .zddc seeded
|
|
os.Setenv("ZDDC_ROOT", root)
|
|
defer os.Unsetenv("ZDDC_ROOT")
|
|
_, err := Load([]string{})
|
|
if err == nil {
|
|
t.Fatal("Load() = nil error, want error about missing root .zddc")
|
|
}
|
|
if !strings.Contains(err.Error(), ".zddc") || !strings.Contains(err.Error(), "publicly accessible") {
|
|
t.Errorf("Load() error = %v, want substring about missing .zddc and public access", err)
|
|
}
|
|
}
|
|
|
|
// TestLoad_MissingRootZddcAllowedWithInsecure: --insecure allows startup
|
|
// when the root .zddc is missing (acknowledges the public-tree shape).
|
|
func TestLoad_MissingRootZddcAllowedWithInsecure(t *testing.T) {
|
|
root := t.TempDir() // no .zddc seeded
|
|
os.Setenv("ZDDC_ROOT", root)
|
|
os.Setenv("ZDDC_INSECURE", "1")
|
|
defer os.Unsetenv("ZDDC_ROOT")
|
|
defer os.Unsetenv("ZDDC_INSECURE")
|
|
cfg, err := Load([]string{})
|
|
if err != nil {
|
|
t.Fatalf("Load() unexpected error: %v", err)
|
|
}
|
|
if !cfg.Insecure {
|
|
t.Errorf("cfg.Insecure = false, want true (set via ZDDC_INSECURE=1)")
|
|
}
|
|
}
|
|
|
|
// TestLoad_MissingRootZddcAllowedWithInsecureFlag: same but via --insecure flag.
|
|
func TestLoad_MissingRootZddcAllowedWithInsecureFlag(t *testing.T) {
|
|
root := t.TempDir() // no .zddc seeded
|
|
cfg, err := Load([]string{"--root", root, "--insecure"})
|
|
if err != nil {
|
|
t.Fatalf("Load() unexpected error: %v", err)
|
|
}
|
|
if !cfg.Insecure {
|
|
t.Errorf("cfg.Insecure = false, want true (set via --insecure)")
|
|
}
|
|
}
|