gateway: add CORS allow-list for the public REST surface
Adds a `GATEWAY_PUBLIC_HTTP_CORS_ALLOWED_ORIGINS` env-driven allow-list on the public REST server so the dev UI on https://www.galaxy.lan can call https://api.galaxy.lan without the browser blocking the cross-origin response. Defaults to empty (no CORS) so the production posture stays closed. The middleware mounts before route classification and anti-abuse, so OPTIONS preflights never charge against per-class rate-limit buckets. `tools/dev-deploy/docker-compose.yml` opts the dev gateway into a single allowed origin (`https://www.galaxy.lan`); local-dev keeps the defaults because Vite proxies through the same origin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,12 @@ const (
|
||||
// the keep-alive idle timeout for the public REST listener.
|
||||
publicHTTPIdleTimeoutEnvVar = "GATEWAY_PUBLIC_HTTP_IDLE_TIMEOUT"
|
||||
|
||||
// publicHTTPCORSAllowedOriginsEnvVar names the environment variable that
|
||||
// configures the comma-separated list of browser origins permitted to
|
||||
// call the public REST surface. An empty value disables CORS entirely;
|
||||
// requests without an Origin header still pass through normally.
|
||||
publicHTTPCORSAllowedOriginsEnvVar = "GATEWAY_PUBLIC_HTTP_CORS_ALLOWED_ORIGINS"
|
||||
|
||||
// publicAuthUpstreamTimeoutEnvVar names the environment variable that
|
||||
// configures the timeout budget used for public auth upstream calls.
|
||||
publicAuthUpstreamTimeoutEnvVar = "GATEWAY_PUBLIC_AUTH_UPSTREAM_TIMEOUT"
|
||||
@@ -457,6 +463,12 @@ type PublicHTTPConfig struct {
|
||||
|
||||
// AntiAbuse configures the public REST anti-abuse middleware.
|
||||
AntiAbuse PublicHTTPAntiAbuseConfig
|
||||
|
||||
// CORSAllowedOrigins is the exact-match list of browser origins
|
||||
// permitted to call the public REST surface. Empty disables CORS:
|
||||
// requests without an Origin header continue to work, cross-origin
|
||||
// requests are subject to the browser's default same-origin policy.
|
||||
CORSAllowedOrigins []string
|
||||
}
|
||||
|
||||
// BackendConfig describes the consolidated backend service the gateway
|
||||
@@ -814,6 +826,16 @@ func LoadFromEnv() (Config, error) {
|
||||
}
|
||||
cfg.PublicHTTP.AuthUpstreamTimeout = publicAuthUpstreamTimeout
|
||||
|
||||
if v, ok := os.LookupEnv(publicHTTPCORSAllowedOriginsEnvVar); ok {
|
||||
origins := make([]string, 0)
|
||||
for part := range strings.SplitSeq(v, ",") {
|
||||
if trimmed := strings.TrimSpace(part); trimmed != "" {
|
||||
origins = append(origins, trimmed)
|
||||
}
|
||||
}
|
||||
cfg.PublicHTTP.CORSAllowedOrigins = origins
|
||||
}
|
||||
|
||||
if v, ok := os.LookupEnv(backendHTTPURLEnvVar); ok {
|
||||
cfg.Backend.HTTPBaseURL = v
|
||||
}
|
||||
|
||||
@@ -158,6 +158,7 @@ func TestLoadFromEnvAppliesPublicAndAuthGRPCDefaults(t *testing.T) {
|
||||
assert.Equal(t, defaultPublicHTTPReadTimeout, cfg.PublicHTTP.ReadTimeout)
|
||||
assert.Equal(t, defaultPublicHTTPIdleTimeout, cfg.PublicHTTP.IdleTimeout)
|
||||
assert.Equal(t, defaultPublicAuthUpstreamTimeout, cfg.PublicHTTP.AuthUpstreamTimeout)
|
||||
assert.Empty(t, cfg.PublicHTTP.CORSAllowedOrigins, "default disables CORS")
|
||||
|
||||
assert.Equal(t, defaultAuthenticatedGRPCAddr, cfg.AuthenticatedGRPC.Addr)
|
||||
assert.Equal(t, defaultAuthenticatedGRPCConnectionTimeout, cfg.AuthenticatedGRPC.ConnectionTimeout)
|
||||
@@ -165,6 +166,22 @@ func TestLoadFromEnvAppliesPublicAndAuthGRPCDefaults(t *testing.T) {
|
||||
assert.Equal(t, defaultAuthenticatedGRPCFreshnessWindow, cfg.AuthenticatedGRPC.FreshnessWindow)
|
||||
}
|
||||
|
||||
func TestLoadFromEnvParsesCORSAllowedOrigins(t *testing.T) {
|
||||
configEnvMu.Lock()
|
||||
defer configEnvMu.Unlock()
|
||||
|
||||
resetEnv(t)
|
||||
setBaseRequiredEnv(t)
|
||||
t.Setenv(publicHTTPCORSAllowedOriginsEnvVar, "https://www.galaxy.lan, , https://staging.galaxy.lan")
|
||||
|
||||
cfg, err := LoadFromEnv()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t,
|
||||
[]string{"https://www.galaxy.lan", "https://staging.galaxy.lan"},
|
||||
cfg.PublicHTTP.CORSAllowedOrigins,
|
||||
"comma-separated list is split, whitespace-trimmed, and empty segments dropped")
|
||||
}
|
||||
|
||||
// resetEnv clears every env var the gateway config might read so that
|
||||
// individual tests can build the exact environment they need without
|
||||
// leakage from a previous test.
|
||||
@@ -179,6 +196,7 @@ func resetEnv(t *testing.T) {
|
||||
publicHTTPReadTimeoutEnvVar,
|
||||
publicHTTPIdleTimeoutEnvVar,
|
||||
publicAuthUpstreamTimeoutEnvVar,
|
||||
publicHTTPCORSAllowedOriginsEnvVar,
|
||||
backendHTTPURLEnvVar,
|
||||
backendGRPCPushURLEnvVar,
|
||||
backendGatewayClientIDEnvVar,
|
||||
|
||||
Reference in New Issue
Block a user