fix(config): add GOCLAW_ALLOWED_ORIGINS env var for CORS config (#543)

Add env var support for gateway.allowed_origins so container deployments
can set CORS origins reliably even if config file is overwritten by UI
save cycles. Follows same pattern as GOCLAW_OWNER_IDS.
This commit is contained in:
viettranx committed 2026-03-30 12:06:51 +07:00
1 parent e27c2cc45e
commit 533ca44e99
3 files changed
+81 -2

No files matched your search

+4 -2
View File
@@ -1140,8 +1140,10 @@ func runGateway() {
if strings.Contains(cfg.Database.PostgresDSN, ":goclaw@") {
slog.Warn("security.default_db_password: using default Postgres password — run ./prepare-env.sh to generate a strong one")
}
if len(cfg.Gateway.AllowedOrigins) == 0 && !edition.Current().IsLimited() {
slog.Warn("security.cors_open: no allowed_origins configured — all WebSocket origins accepted. Set gateway.allowed_origins for production")
if len(cfg.Gateway.AllowedOrigins) > 0 {
slog.Info("cors: allowed_origins configured", "origins", cfg.Gateway.AllowedOrigins)
} else if !edition.Current().IsLimited() {
slog.Warn("security.cors_open: no allowed_origins configured — all WebSocket origins accepted. Set gateway.allowed_origins or GOCLAW_ALLOWED_ORIGINS for production")
}
if err := server.Start(ctx); err != nil {
+11
View File
@@ -214,6 +214,17 @@ func (c *Config) applyEnvOverrides() {
c.Gateway.OwnerIDs = ids
}
// Allowed origins from env (comma-separated, whitespace-trimmed)
if v := os.Getenv("GOCLAW_ALLOWED_ORIGINS"); v != "" {
var origins []string
for origin := range strings.SplitSeq(v, ",") {
if trimmed := strings.TrimSpace(origin); trimmed != "" {
origins = append(origins, trimmed)
}
}
c.Gateway.AllowedOrigins = origins
}
// Tailscale (tsnet)
envStr("GOCLAW_TSNET_HOSTNAME", &c.Tailscale.Hostname)
envStr("GOCLAW_TSNET_AUTH_KEY", &c.Tailscale.AuthKey)
+66
View File
@@ -132,6 +132,72 @@ func TestLoad_EnvVarAPIKeys(t *testing.T) {
}
}
// --- Allowed origins from JSON5 ---
func TestLoad_AllowedOrigins_JSON5(t *testing.T) {
dir := t.TempDir()
cfgPath := filepath.Join(dir, "config.json5")
content := `{
"gateway": {
"allowed_origins": [
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3002",
],
},
}`
os.WriteFile(cfgPath, []byte(content), 0644)
cfg, err := Load(cfgPath)
if err != nil {
t.Fatalf("load error: %v", err)
}
if len(cfg.Gateway.AllowedOrigins) != 3 {
t.Fatalf("expected 3 origins, got %d: %v", len(cfg.Gateway.AllowedOrigins), cfg.Gateway.AllowedOrigins)
}
if cfg.Gateway.AllowedOrigins[0] != "https://app.example.com" {
t.Fatalf("first origin: got %q", cfg.Gateway.AllowedOrigins[0])
}
if cfg.Gateway.AllowedOrigins[2] != "http://localhost:3002" {
t.Fatalf("third origin: got %q", cfg.Gateway.AllowedOrigins[2])
}
}
// --- Allowed origins from env var ---
func TestLoad_AllowedOrigins_EnvVar(t *testing.T) {
t.Setenv("GOCLAW_ALLOWED_ORIGINS", " https://a.com , https://b.com ")
cfg, err := Load("/nonexistent/path")
if err != nil {
t.Fatalf("load error: %v", err)
}
if len(cfg.Gateway.AllowedOrigins) != 2 {
t.Fatalf("expected 2 origins, got %d: %v", len(cfg.Gateway.AllowedOrigins), cfg.Gateway.AllowedOrigins)
}
if cfg.Gateway.AllowedOrigins[0] != "https://a.com" || cfg.Gateway.AllowedOrigins[1] != "https://b.com" {
t.Fatalf("origins not parsed correctly: %v", cfg.Gateway.AllowedOrigins)
}
}
func TestLoad_AllowedOrigins_EnvVar_OverridesFile(t *testing.T) {
dir := t.TempDir()
cfgPath := filepath.Join(dir, "config.json5")
os.WriteFile(cfgPath, []byte(`{"gateway":{"allowed_origins":["https://file.com"]}}`), 0644)
// Env var should override file value
t.Setenv("GOCLAW_ALLOWED_ORIGINS", "https://env.com")
cfg, err := Load(cfgPath)
if err != nil {
t.Fatalf("load error: %v", err)
}
if len(cfg.Gateway.AllowedOrigins) != 1 || cfg.Gateway.AllowedOrigins[0] != "https://env.com" {
t.Fatalf("env should override file: got %v", cfg.Gateway.AllowedOrigins)
}
}
// --- FlexibleStringSlice ---
func TestFlexibleStringSlice_StringArray(t *testing.T) {