console: attach-door host policy — shape + never platform space
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 files changed,
+57
−0
internal/console/domainpolicy.go+30 −0internal/console/domainpolicy_test.go+27 −0
diff --git a/internal/console/domainpolicy.go b/internal/console/domainpolicy.go| new file mode 100644 |
| index 000000000..c29025cf0 |
| --- /dev/null |
| +++ b/internal/console/domainpolicy.go |
| @@ -0,0 +1,30 @@ |
| +// Attach-door host policy (#82). #151 exports the shared validation spot |
| +// for the domains door; when the branches merge these checks land there |
| +// (Task 8) — the rules themselves are this file's contract: |
| +// - hostname shape only (ValidHostShape — apexes allowed, lowercase), |
| +// - never platform space: an attached domain is a foreign domain by |
| +// definition, so anything equal to or under the deployment's apps |
| +// domain is refused before any claim is written. |
| +// |
| +// First-writer-wins is NOT here: the claim's create-only write is the |
| +// entire uniqueness check, and its refusal is the door's neutral |
| +// "host unavailable". |
| +package console |
| + |
| +import ( |
| + "strings" |
| + |
| + "github.com/carlosframework/platform/internal/provision" |
| +) |
| + |
| +func attachHostRefusal(host, appsDomain string) string { |
| + if !provision.ValidHostShape(host) { |
| + return "not a valid hostname" |
| + } |
| + if appsDomain != "" { |
| + if host == appsDomain || strings.HasSuffix(host, "."+appsDomain) { |
| + return "hosts under the platform's apps domain are assigned by the platform, not attached" |
| + } |
| + } |
| + return "" |
| +} |
diff --git a/internal/console/domainpolicy_test.go b/internal/console/domainpolicy_test.go| new file mode 100644 |
| index 000000000..de91a39ce |
| --- /dev/null |
| +++ b/internal/console/domainpolicy_test.go |
| @@ -0,0 +1,27 @@ |
| +// internal/console/domainpolicy_test.go |
| +package console |
| + |
| +import "testing" |
| + |
| +func TestAttachHostRefusal(t *testing.T) { |
| + cases := []struct { |
| + name, host, appsDomain string |
| + wantAllowed bool |
| + }{ |
| + {"plain custom fqdn", "www.example.com", "oncarlos.com", true}, |
| + {"apex", "example.com", "oncarlos.com", true}, |
| + {"uppercase rejected", "WWW.example.com", "oncarlos.com", false}, |
| + {"empty host", "", "oncarlos.com", false}, |
| + {"traversal shape", "../control/serial", "oncarlos.com", false}, |
| + {"apps domain itself", "oncarlos.com", "oncarlos.com", false}, |
| + {"under apps domain", "story.bdj.oncarlos.com", "oncarlos.com", false}, |
| + {"apps domain unset allows customs", "www.example.com", "", true}, |
| + {"suffix but not label boundary", "notoncarlos.com", "oncarlos.com", true}, |
| + } |
| + for _, c := range cases { |
| + got := attachHostRefusal(c.host, c.appsDomain) |
| + if (got == "") != c.wantAllowed { |
| + t.Errorf("%s: attachHostRefusal(%q,%q) = %q, want allowed=%v", c.name, c.host, c.appsDomain, got, c.wantAllowed) |
| + } |
| + } |
| +} |