carlos / platform Public

provision: close three door-validation gaps in ValidateShape

Fix three findings from coordinator review:

1. Addr port validation: net.SplitHostPort accepts garbage ports
   (e.g. "127.0.0.1:abc"). Parse port as uint16 and reject if not
   in range 1..65535, refusing materialization of routes that never
   connect (the -backing unti incident class).

2. Environment validation: Environment field had no door check but
   reaches bucket key construction in appconfig.EnvironmentKey.
   Validate using appconfig.ValidEnvironmentName to reject invalid
   environment names before they bind silently to wrong bundles.

3. Service kind requires upstream: A service record without Addr
   contradicts the design ("the platform only proxies to a service"),
   so refuse it at the door. Also validates DerivesPaths correctly
   for services (always have Addr, never socket-backed).

Add three test cases: bad port, bad environment name, and service
with no addr.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell 0d6ee3c9bc6f0d1f3de6643d7341b09c5260ddb1 parent 7d5e81d
2 files changed, +20 −0
  • internal/provision/provision.go +16 −0
  • internal/provision/provision_test.go +4 −0
diff --git a/internal/provision/provision.go b/internal/provision/provision.go
index b622b55bb..2e9f97dbe 100644
--- a/internal/provision/provision.go
+++ b/internal/provision/provision.go
@@ -16,9 +16,11 @@ import (
"path/filepath"
"regexp"
"sort"
+ "strconv"
"strings"
"time"
+ "github.com/carlosframework/platform/internal/appconfig"
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
)
@@ -231,10 +233,21 @@ func ValidateShape(rec Record) error {
if err != nil || h == "" || p == "" {
return fmt.Errorf("addr %q: want a TCP upstream as host:port (e.g. 127.0.0.1:8090)", rec.Addr)
}
+ // Parse port as integer and validate range 1..65535. net.SplitHostPort
+ // accepts garbage ports (e.g. "127.0.0.1:abc") — we need to refuse them
+ // here at the door, not discover them months later as a route that never
+ // connects (the -backing unti class of incident).
+ portNum, err := strconv.ParseUint(p, 10, 16)
+ if err != nil || portNum == 0 {
+ return fmt.Errorf("addr %q: port must be an integer in the range 1..65535", rec.Addr)
+ }
}
if rec.Unit != "" && !registry.ValidUnitName(rec.Unit) {
return fmt.Errorf("unit %q: want a systemd service unit name (e.g. console.service)", rec.Unit)
}
+ if rec.Environment != "" && !appconfig.ValidEnvironmentName(rec.Environment) {
+ return fmt.Errorf("environment %q: want [a-z0-9-]{1,32}", rec.Environment)
+ }
if rec.KindOrDefault() == "static" {
if rec.Addr != "" {
return errors.New("kind static with addr: a static route serves a file tree and proxies to nothing — drop one of the two")
@@ -246,6 +259,9 @@ func ValidateShape(rec Record) error {
return errors.New("kind static with hibernate: a static route has no process to wake, so arming the activator for it would wedge the host — drop hibernate")
}
}
+ if rec.KindOrDefault() == "service" && rec.Addr == "" {
+ return errors.New("kind service with no addr: the platform only proxies to a service, so the record must name the upstream it proxies to")
+ }
return nil
}
diff --git a/internal/provision/provision_test.go b/internal/provision/provision_test.go
index c96287e1e..d7b8b4f08 100644
--- a/internal/provision/provision_test.go
+++ b/internal/provision/provision_test.go
@@ -468,6 +468,7 @@ func TestValidateShape(t *testing.T) {
{"legacy instance", base},
{"addr tenant", with(func(r *Record) { r.Addr = "127.0.0.1:8090" })},
{"unit-owned tenant", with(func(r *Record) { r.Addr = "127.0.0.1:8090"; r.Unit = "console.service" })},
+ {"service with addr", with(func(r *Record) { r.Kind = "service"; r.Addr = "127.0.0.1:8090" })},
{"static", with(func(r *Record) { r.Kind = "static" })},
{"explicitly non-hibernating instance", with(func(r *Record) { r.Hibernate = new(bool) })},
}
@@ -483,7 +484,10 @@ func TestValidateShape(t *testing.T) {
{"bogus kind", with(func(r *Record) { r.Kind = "unti" })},
{"status kind", with(func(r *Record) { r.Kind = "status" })},
{"addr without port", with(func(r *Record) { r.Addr = "127.0.0.1" })},
+ {"addr with garbage port", with(func(r *Record) { r.Addr = "127.0.0.1:abc" })},
{"unit typo", with(func(r *Record) { r.Unit = "console" })},
+ {"bad environment name", with(func(r *Record) { r.Environment = "Demo" })},
+ {"service with no addr", with(func(r *Record) { r.Kind = "service" })},
{"static with addr", with(func(r *Record) { r.Kind = "static"; r.Addr = "127.0.0.1:8090" })},
{"static with unit", with(func(r *Record) { r.Kind = "static"; r.Unit = "console.service" })},
{"static hibernating", with(func(r *Record) { r.Kind = "static"; r.Hibernate = &yes })},