| new file mode 100644 |
| index 000000000..576bb6c5d |
| --- /dev/null |
| +++ b/internal/console/domainpolicy_test.go |
| @@ -0,0 +1,270 @@ |
| +package console |
| + |
| +import ( |
| + "context" |
| + "net/http" |
| + "strings" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/domains" |
| + "github.com/carlosframework/platform/internal/edgesfile" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/status" |
| +) |
| + |
| +func TestDirectAppsDomainHost(t *testing.T) { |
| + cases := []struct { |
| + name string |
| + appsDomain string |
| + host string |
| + want bool |
| + }{ |
| + {"no apps domain configured", "", "widget.apps.test", false}, |
| + {"the apex itself is not a one-label alias", "apps.test", "apps.test", false}, |
| + {"one label under the apps domain", "apps.test", "widget.apps.test", true}, |
| + {"a nested name is refused", "apps.test", "a.b.apps.test", false}, |
| + {"a host unrelated to the apps domain", "apps.test", "widget.other.test", false}, |
| + {"a suffix match without the separating dot", "apps.test", "notapps.test", false}, |
| + } |
| + for _, c := range cases { |
| + t.Run(c.name, func(t *testing.T) { |
| + s := &Server{cfg: Config{AppsDomain: c.appsDomain}} |
| + if got := s.directAppsDomainHost(c.host); got != c.want { |
| + t.Errorf("directAppsDomainHost(%q) with AppsDomain=%q = %v, want %v", c.host, c.appsDomain, got, c.want) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestDomainHostDeniedPolicyMatrix(t *testing.T) { |
| + const appsDomain = "apps.test" |
| + const alias = "widget." + appsDomain |
| + base := func() Config { |
| + return Config{ |
| + AppsDomain: appsDomain, |
| + Origin: "https://origin." + appsDomain, |
| + DenyDomains: []string{"blocked." + appsDomain}, |
| + RedirectHosts: []string{"retired." + appsDomain}, |
| + } |
| + } |
| + cases := []struct { |
| + name string |
| + mutate func(*Config) |
| + host string |
| + want bool |
| + }{ |
| + {"flag off denies a one-label alias, unchanged from before the flag existed", |
| + func(c *Config) {}, alias, true}, |
| + {"flag on allows an ordinary one-label alias", |
| + func(c *Config) { c.AppsDomainAliasAccounts = []string{"bab"} }, alias, false}, |
| + {"flag on still denies the apex", |
| + func(c *Config) { c.AppsDomainAliasAccounts = []string{"bab"} }, appsDomain, true}, |
| + {"flag on still denies a nested name", |
| + func(c *Config) { c.AppsDomainAliasAccounts = []string{"bab"} }, "a.b." + appsDomain, true}, |
| + {"flag on still denies an explicit DenyDomains entry", |
| + func(c *Config) { c.AppsDomainAliasAccounts = []string{"bab"} }, "blocked." + appsDomain, true}, |
| + {"flag on still denies the console's own origin host", |
| + func(c *Config) { c.AppsDomainAliasAccounts = []string{"bab"} }, "origin." + appsDomain, true}, |
| + {"flag on still denies a retired redirect host", |
| + func(c *Config) { c.AppsDomainAliasAccounts = []string{"bab"} }, "retired." + appsDomain, true}, |
| + {"flag on with no apps domain configured denies nothing new", |
| + func(c *Config) { c.AppsDomainAliasAccounts = []string{"bab"}; c.AppsDomain = "" }, "widget.example.com", false}, |
| + } |
| + for _, c := range cases { |
| + t.Run(c.name, func(t *testing.T) { |
| + cfg := base() |
| + c.mutate(&cfg) |
| + s := &Server{cfg: cfg} |
| + if got := s.domainHostDenied(c.host, "bab"); got != c.want { |
| + t.Errorf("domainHostDenied(%q) = %v, want %v (cfg=%+v)", c.host, got, c.want, cfg) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestAPIDomainAttachDeniesAppsDomainAliasByDefault(t *testing.T) { |
| + fx, member := instanceFixture(t, "code-dom-alias-default") |
| + fx.srv.cfg.AppsDomain = "oncarlos.test" |
| + code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), member, |
| + attachBody("widget.oncarlos.test", "go.tito.io")) |
| + if code != http.StatusBadRequest { |
| + t.Fatalf("attach of an apps-domain alias with the flag unset = %d %s, want 400", code, out) |
| + } |
| + claimAbsent(t, fx, "widget.oncarlos.test") |
| +} |
| + |
| +func TestAPIDomainAttachAllowsAppsDomainAlias(t *testing.T) { |
| + fx, member := instanceFixture(t, "code-dom-alias-allow") |
| + fx.srv.cfg.AppsDomain = "oncarlos.test" |
| + fx.srv.cfg.AppsDomainAliasAccounts = []string{fx.sqid} |
| + |
| + if got := attachDomain(t, fx, member, "widget.oncarlos.test", "go.tito.io"); !got.Created { |
| + t.Fatalf("attach of a one-label apps-domain alias = %+v, want created true", got) |
| + } |
| + |
| + for _, host := range []string{ |
| + "a.widget.oncarlos.test", // nested |
| + "oncarlos.test", // the apex itself |
| + } { |
| + code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), member, |
| + attachBody(host, "go.tito.io")) |
| + if code != http.StatusBadRequest { |
| + t.Errorf("attach %q with aliases allowed = %d %s, want 400", host, code, out) |
| + } |
| + claimAbsent(t, fx, host) |
| + } |
| +} |
| + |
| +func TestAPIDomainAttachAppsDomainAliasRejectsWildcardAndCatchall(t *testing.T) { |
| + fx, member := instanceFixture(t, "code-dom-alias-wild") |
| + fx.srv.cfg.AppsDomain = "oncarlos.test" |
| + fx.srv.cfg.AppsDomainAliasAccounts = []string{fx.sqid} |
| + const host = "widget.oncarlos.test" |
| + |
| + for name, body := range map[string]string{ |
| + "wildcard": `{"host":"` + host + `","route":"go.tito.io","wildcard":true}`, |
| + "catchall": `{"host":"` + host + `","route":"go.tito.io","catchall":true}`, |
| + } { |
| + code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), member, body) |
| + if code != http.StatusBadRequest { |
| + t.Errorf("%s flag on an apps-domain alias = %d %s, want 400", name, code, out) |
| + } |
| + claimAbsent(t, fx, host) |
| + } |
| + if got := attachDomain(t, fx, member, host, "go.tito.io"); !got.Created { |
| + t.Fatalf("plain attach of the same host with neither flag set = %+v, want created true", got) |
| + } |
| +} |
| + |
| +func TestAPIDomainAttachAppsDomainAliasStillRefusesForeignClaim(t *testing.T) { |
| + fx, member := instanceFixture(t, "code-dom-alias-conflict") |
| + fx.srv.cfg.AppsDomain = "oncarlos.test" |
| + fx.srv.cfg.AppsDomainAliasAccounts = []string{fx.sqid} |
| + const host = "widget.oncarlos.test" |
| + if _, _, err := domains.AttachClaim(context.Background(), fx.rel, domains.Claim{ |
| + Host: host, Account: "cdef", App: "widgets", |
| + Route: "widgets.example.net", CreatedAt: time.Now().UTC().Format(time.RFC3339), |
| + }); err != nil { |
| + t.Fatal(err) |
| + } |
| + code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), member, |
| + attachBody(host, "go.tito.io")) |
| + if code != http.StatusConflict { |
| + t.Fatalf("attach of an apps-domain alias another account holds = %d %s, want 409", code, out) |
| + } |
| + if got := strings.TrimSpace(out); got != `{"error":"hostname unavailable"}` { |
| + t.Fatalf("409 body = %s, want the neutral refusal", got) |
| + } |
| +} |
| + |
| +func TestAPIDomainAttachAppsDomainAliasStillRefusesDeploymentRoute(t *testing.T) { |
| + ctx := context.Background() |
| + fx, member := instanceFixture(t, "code-dom-alias-route") |
| + fx.srv.cfg.AppsDomain = "oncarlos.test" |
| + fx.srv.cfg.AppsDomainAliasAccounts = []string{fx.sqid} |
| + const host = "widget.oncarlos.test" |
| + if _, err := fx.rel.Put(ctx, edgesfile.Key("annie"), |
| + []byte(`{"v":1,"box":"annie","eip":"198.51.100.9","hosts":{"`+host+`":{"role":"home"}}}`), ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := status.WriteBox(ctx, fx.rel, status.Box{Label: "annie", GeneratedAt: time.Now()}); err != nil { |
| + t.Fatal(err) |
| + } |
| + code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), member, |
| + attachBody(host, "two.go.tito.io")) |
| + if code != http.StatusConflict { |
| + t.Fatalf("attach of an apps-domain alias a live box already serves = %d %s, want 409", code, out) |
| + } |
| + claimAbsent(t, fx, host) |
| +} |
| + |
| +func TestAttachDomainCoreAppsDomainAlias(t *testing.T) { |
| + fx, acct := attachFixture(t) |
| + fx.srv.cfg.Registry = attachRegistry(t, |
| + registry.Route{Host: "story." + fx.sqid + ".oncarlos.com", Kind: "static", App: "story", |
| + Account: fx.sqid, Channel: "canary/live", Status: "ready"}, |
| + ) |
| + const alias = "widget.oncarlos.com" |
| + |
| + if _, ref := fx.srv.attachDomain(context.Background(), acct, "story", alias, "", "", "paul@keymail.dev"); ref == nil || ref.Status != http.StatusUnprocessableEntity { |
| + t.Fatalf("attach of an apps-domain alias with the flag unset = %+v, want 422", ref) |
| + } |
| + claimAbsent(t, fx, alias) |
| + |
| + fx.srv.cfg.AppsDomainAliasAccounts = []string{fx.sqid} |
| + res, ref := fx.srv.attachDomain(context.Background(), acct, "story", alias, "", "", "paul@keymail.dev") |
| + if ref != nil { |
| + t.Fatalf("attach of an apps-domain alias with the flag set = refused %+v", ref) |
| + } |
| + want := "story." + fx.sqid + ".oncarlos.com" |
| + if res.Claim.Route != want { |
| + t.Fatalf("claim route = %q, want the ready route %q it aliases", res.Claim.Route, want) |
| + } |
| + |
| + if _, ref := fx.srv.attachDomain(context.Background(), acct, "story", "a.b.oncarlos.com", "", "", "paul@keymail.dev"); ref == nil || ref.Status != http.StatusUnprocessableEntity { |
| + t.Fatalf("attach of a nested apps-domain name with the flag set = %+v, want still 422", ref) |
| + } |
| + claimAbsent(t, fx, "a.b.oncarlos.com") |
| +} |
| + |
| +func TestAttachDomainCoreAppsDomainAliasStillRefusesHeldHost(t *testing.T) { |
| + fx, acct := attachFixture(t) |
| + fx.srv.cfg.AppsDomainAliasAccounts = []string{fx.sqid} |
| + fx.srv.cfg.Registry = attachRegistry(t, |
| + registry.Route{Host: "story." + fx.sqid + ".oncarlos.com", Kind: "static", App: "story", |
| + Account: fx.sqid, Channel: "canary/live", Status: "ready"}, |
| + ) |
| + const alias = "widget.oncarlos.com" |
| + seedClaim(t, fx, domains.Claim{Schema: 1, Host: alias, Account: "zzz", App: "rival"}) |
| + |
| + _, ref := fx.srv.attachDomain(context.Background(), acct, "story", alias, "", "", "paul@keymail.dev") |
| + if ref == nil || ref.Status != http.StatusConflict || ref.Msg != "hostname unavailable" { |
| + t.Fatalf("refusal = %+v, want exactly 409 hostname unavailable even though aliases are allowed", ref) |
| + } |
| +} |
| + |
| +func TestAppsDomainAliasAccountAuthority(t *testing.T) { |
| + s := &Server{cfg: Config{ |
| + AppsDomain: "platform.example.test", AppsDomainAliasAccounts: []string{"bap"}, |
| + Origin: "https://console.platform.example.test", |
| + DenyDomains: []string{"example.test", "platform.example.test", "blocked.platform.example.test"}, |
| + }} |
| + for _, tc := range []struct { |
| + host, account string |
| + denied bool |
| + }{ |
| + {"pullup.platform.example.test", "bap", false}, |
| + {"pullup.platform.example.test", "bab", true}, |
| + {"pullup.platform.example.test", "", true}, |
| + {"bap.platform.example.test", "bap", true}, |
| + {"bab.platform.example.test", "bap", true}, |
| + {"edge.platform.example.test", "bap", true}, |
| + {"console.platform.example.test", "bap", true}, |
| + {"blocked.platform.example.test", "bap", true}, |
| + {"x.bap.platform.example.test", "bap", true}, |
| + {"platform.example.test", "bap", true}, |
| + {"outside.example.test", "bap", true}, |
| + } { |
| + if got := s.domainHostDenied(tc.host, tc.account); got != tc.denied { |
| + t.Errorf("%s/%s: denied=%v, want %v", tc.account, tc.host, got, tc.denied) |
| + } |
| + } |
| + s.cfg.SteeringHost = "steering.platform.example.test" |
| + if !s.domainHostDenied(s.cfg.SteeringHost, "bap") { |
| + t.Fatal("steering host allowed") |
| + } |
| +} |
| + |
| +func TestAppsDomainAliasCannotMintFirstRoute(t *testing.T) { |
| + fx, acct := attachFixture(t) |
| + fx.srv.cfg.AppsDomainAliasAccounts = []string{fx.sqid} |
| + fx.srv.cfg.Registry = attachRegistry(t) |
| + for _, kind := range []string{"", "static"} { |
| + _, ref := fx.srv.attachDomain(context.Background(), acct, "story", "pullup.oncarlos.com", kind, "", "actor") |
| + if ref == nil || ref.Status != http.StatusUnprocessableEntity { |
| + t.Fatalf("kind %q: refusal=%v", kind, ref) |
| + } |
| + claimAbsent(t, fx, "pullup.oncarlos.com") |
| + } |
| +} |