fix: allow explicit external endpoints in nonprod real deployments
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNonprodRealAcceptsConfiguredExternalEndpointsOnly(t *testing.T) {
|
||||
for _, tc := range []struct{ saas, mq string }{
|
||||
{"http://saas.example.invalid", "amqp://synthetic:placeholder@mq.example.invalid:5672/test"},
|
||||
{"https://saas.example.invalid", "amqps://synthetic:placeholder@mq.example.invalid:5671/test"},
|
||||
} {
|
||||
path := setCurrentDispatcherRuntimeEnvironment(t)
|
||||
t.Setenv("SAAS_BASE_URL", tc.saas)
|
||||
t.Setenv("RABBITMQ_URL", tc.mq)
|
||||
settings, err := LoadDispatcherRuntimeEnvironment("nonprod-real")
|
||||
if err != nil || settings.SaaSBaseURL != tc.saas || settings.RabbitMQURL != tc.mq {
|
||||
t.Fatalf("explicit endpoints were rejected or rewritten: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("validation opened SQLite: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMockAndSIPOnlyStillRejectExternalEndpoints(t *testing.T) {
|
||||
for _, mode := range []string{"mock", "sip-only"} {
|
||||
for _, name := range []string{"SAAS_BASE_URL", "RABBITMQ_URL"} {
|
||||
t.Run(mode+"/"+name, func(t *testing.T) {
|
||||
setDispatcherEnvironment(t)
|
||||
value := "https://external.example.invalid"
|
||||
if name == "RABBITMQ_URL" {
|
||||
value = "amqps://synthetic:placeholder@external.example.invalid/test"
|
||||
}
|
||||
t.Setenv(name, value)
|
||||
if _, err := LoadDispatcherEnvironment(mode); err == nil || strings.Contains(err.Error(), value) {
|
||||
t.Fatalf("isolated mode accepted an external address or echoed it: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonprodRealRejectsMalformedEndpointsWithoutLeakingConfiguration(t *testing.T) {
|
||||
for _, tc := range []struct{ name, value string }{
|
||||
{"SAAS_BASE_URL", "not-a-url"},
|
||||
{"SAAS_BASE_URL", "ftp://external.example.invalid"},
|
||||
{"SAAS_BASE_URL", "https://external.example.invalid?secret=placeholder"},
|
||||
{"RABBITMQ_URL", "https://synthetic:placeholder@external.example.invalid"},
|
||||
{"RABBITMQ_URL", "amqp://synthetic:placeholder@external.example.invalid#fragment"},
|
||||
} {
|
||||
t.Run(tc.name+"/"+tc.value, func(t *testing.T) {
|
||||
path := setDispatcherEnvironment(t)
|
||||
t.Setenv(tc.name, tc.value)
|
||||
if _, err := LoadDispatcherEnvironment("nonprod-real"); err == nil || !strings.Contains(err.Error(), tc.name) || strings.Contains(err.Error(), tc.value) || strings.Contains(err.Error(), "placeholder") {
|
||||
t.Fatalf("malformed endpoint was accepted or exposed: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("rejected configuration opened SQLite: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@ type DispatcherEnvironment struct {
|
||||
}
|
||||
|
||||
// LoadDispatcherEnvironment is pure inspection: it opens no database, queue or
|
||||
// network connection. Nonproduction real calls still use only the local SaaS
|
||||
// simulator and predeclared local AMQP test queues.
|
||||
// network connection. Mock/SIP-only remain local; explicitly authorized
|
||||
// nonprod-real deployments use their configured SaaS and AMQP endpoints.
|
||||
func LoadDispatcherEnvironment(mode string) (DispatcherEnvironment, error) {
|
||||
if mode != "mock" && mode != "sip-only" && mode != "nonprod-real" {
|
||||
return DispatcherEnvironment{}, errors.New("Dispatcher accepts only isolated Mock, SIP-only or explicit nonprod-real mode")
|
||||
@@ -49,14 +49,14 @@ func LoadDispatcherEnvironment(mode string) (DispatcherEnvironment, error) {
|
||||
if settings.SaaSBaseURL, err = get("SAAS_BASE_URL"); err != nil {
|
||||
return DispatcherEnvironment{}, err
|
||||
}
|
||||
if !localEndpoint(settings.SaaSBaseURL, "http", "https") {
|
||||
return DispatcherEnvironment{}, errors.New("SAAS_BASE_URL must name a local Mock HTTP endpoint")
|
||||
if !endpoint(settings.SaaSBaseURL, mode != "nonprod-real", "http", "https") {
|
||||
return DispatcherEnvironment{}, errors.New("SAAS_BASE_URL must be a valid HTTP endpoint; isolated modes require loopback")
|
||||
}
|
||||
if settings.RabbitMQURL, err = get("RABBITMQ_URL"); err != nil {
|
||||
return DispatcherEnvironment{}, err
|
||||
}
|
||||
if !localEndpoint(settings.RabbitMQURL, "amqp", "amqps") {
|
||||
return DispatcherEnvironment{}, errors.New("RABBITMQ_URL must name a local Mock AMQP endpoint")
|
||||
if !endpoint(settings.RabbitMQURL, mode != "nonprod-real", "amqp", "amqps") {
|
||||
return DispatcherEnvironment{}, errors.New("RABBITMQ_URL must be a valid AMQP endpoint; isolated modes require loopback")
|
||||
}
|
||||
if settings.SQLitePath, err = get("DISPATCHER_SQLITE_PATH"); err != nil {
|
||||
return DispatcherEnvironment{}, err
|
||||
@@ -65,6 +65,10 @@ func LoadDispatcherEnvironment(mode string) (DispatcherEnvironment, error) {
|
||||
}
|
||||
|
||||
func localEndpoint(raw string, schemes ...string) bool {
|
||||
return endpoint(raw, true, schemes...)
|
||||
}
|
||||
|
||||
func endpoint(raw string, localOnly bool, schemes ...string) bool {
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil || parsed.Hostname() == "" || parsed.Opaque != "" || parsed.Fragment != "" || parsed.RawQuery != "" {
|
||||
return false
|
||||
@@ -76,6 +80,9 @@ func localEndpoint(raw string, schemes ...string) bool {
|
||||
if !validScheme {
|
||||
return false
|
||||
}
|
||||
if !localOnly {
|
||||
return true
|
||||
}
|
||||
host := parsed.Hostname()
|
||||
if strings.EqualFold(host, "localhost") {
|
||||
return true
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestLoadDispatcherEnvironmentRefusesNonMockBeforeResources(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDispatcherEnvironmentNonprodRealUsesOnlyLocalSaaSMockAndQueue(t *testing.T) {
|
||||
func TestLoadDispatcherEnvironmentNonprodRealPreservesExplicitEndpointsWithoutIO(t *testing.T) {
|
||||
path := setDispatcherEnvironment(t)
|
||||
settings, err := LoadDispatcherEnvironment("nonprod-real")
|
||||
if err != nil || settings.SQLitePath != path {
|
||||
@@ -44,8 +44,13 @@ func TestLoadDispatcherEnvironmentNonprodRealUsesOnlyLocalSaaSMockAndQueue(t *te
|
||||
t.Fatalf("real configuration inspection opened SQLite: %v", err)
|
||||
}
|
||||
t.Setenv("SAAS_BASE_URL", "https://saas.example.invalid")
|
||||
if _, err := LoadDispatcherEnvironment("nonprod-real"); err == nil {
|
||||
t.Fatal("external SaaS is not authorized for this nonproduction test")
|
||||
t.Setenv("RABBITMQ_URL", "amqps://synthetic:placeholder@mq.example.invalid:5671/%2F")
|
||||
settings, err = LoadDispatcherEnvironment("nonprod-real")
|
||||
if err != nil || settings.SaaSBaseURL != "https://saas.example.invalid" || settings.RabbitMQURL != "amqps://synthetic:placeholder@mq.example.invalid:5671/%2F" {
|
||||
t.Fatalf("approved external deployment values were rejected or changed: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("external configuration inspection opened SQLite: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user