| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "net/http" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/domains" |
| 11 | "github.com/carlosframework/platform/internal/edgesfile" |
| 12 | "github.com/carlosframework/platform/internal/registry" |
| 13 | "github.com/carlosframework/platform/internal/status" |
| 14 | ) |
| 15 | |
| 16 | func TestDirectAppsDomainHost(t *testing.T) { |
| 17 | cases := []struct { |
| 18 | name string |
| 19 | appsDomain string |
| 20 | host string |
| 21 | want bool |
| 22 | }{ |
| 23 | {"no apps domain configured", "", "widget.apps.test", false}, |
| 24 | {"the apex itself is not a one-label alias", "apps.test", "apps.test", false}, |
| 25 | {"one label under the apps domain", "apps.test", "widget.apps.test", true}, |
| 26 | {"a nested name is refused", "apps.test", "a.b.apps.test", false}, |
| 27 | {"a host unrelated to the apps domain", "apps.test", "widget.other.test", false}, |
| 28 | {"a suffix match without the separating dot", "apps.test", "notapps.test", false}, |
| 29 | } |
| 30 | for _, c := range cases { |
| 31 | t.Run(c.name, func(t *testing.T) { |
| 32 | s := &Server{cfg: Config{AppsDomain: c.appsDomain, ReservePlatformDomains: true}} |
| 33 | if got := s.directAppsDomainHost(c.host); got != c.want { |
| 34 | t.Errorf("directAppsDomainHost(%q) with AppsDomain=%q = %v, want %v", c.host, c.appsDomain, got, c.want) |
| 35 | } |
| 36 | }) |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | func TestDomainHostDeniedPolicyMatrix(t *testing.T) { |
| 41 | const appsDomain = "apps.test" |
| 42 | const alias = "widget." + appsDomain |
| 43 | base := func() Config { |
| 44 | return Config{ |
| 45 | AppsDomain: appsDomain, |
| 46 | ReservePlatformDomains: true, |
| 47 | Origin: "https://origin." + appsDomain, |
| 48 | DenyDomains: []string{"blocked." + appsDomain}, |
| 49 | RedirectHosts: []string{"retired." + appsDomain}, |
| 50 | } |
| 51 | } |
| 52 | cases := []struct { |
| 53 | name string |
| 54 | mutate func(*Config) |
| 55 | host string |
| 56 | want bool |
| 57 | }{ |
| 58 | {"flag off denies a one-label alias, unchanged from before the flag existed", |
| 59 | func(c *Config) {}, alias, true}, |
| 60 | {"flag on allows an ordinary one-label alias", |
| 61 | func(c *Config) { c.AppsDomainAliasAccounts = []string{"bab"} }, alias, false}, |
| 62 | {"flag on still denies the apex", |
| 63 | func(c *Config) { c.AppsDomainAliasAccounts = []string{"bab"} }, appsDomain, true}, |
| 64 | {"flag on still denies a nested name", |
| 65 | func(c *Config) { c.AppsDomainAliasAccounts = []string{"bab"} }, "a.b." + appsDomain, true}, |
| 66 | {"flag on still denies an explicit DenyDomains entry", |
| 67 | func(c *Config) { c.AppsDomainAliasAccounts = []string{"bab"} }, "blocked." + appsDomain, true}, |
| 68 | {"flag on still denies the console's own origin host", |
| 69 | func(c *Config) { c.AppsDomainAliasAccounts = []string{"bab"} }, "origin." + appsDomain, true}, |
| 70 | {"flag on still denies a retired redirect host", |
| 71 | func(c *Config) { c.AppsDomainAliasAccounts = []string{"bab"} }, "retired." + appsDomain, true}, |
| 72 | {"flag on with no apps domain configured denies nothing new", |
| 73 | func(c *Config) { c.AppsDomainAliasAccounts = []string{"bab"}; c.AppsDomain = "" }, "widget.example.com", false}, |
| 74 | } |
| 75 | for _, c := range cases { |
| 76 | t.Run(c.name, func(t *testing.T) { |
| 77 | cfg := base() |
| 78 | c.mutate(&cfg) |
| 79 | s := &Server{cfg: cfg} |
| 80 | if got := s.domainHostDenied(c.host, "bab"); got != c.want { |
| 81 | t.Errorf("domainHostDenied(%q) = %v, want %v (cfg=%+v)", c.host, got, c.want, cfg) |
| 82 | } |
| 83 | }) |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | func TestAPIDomainAttachDeniesAppsDomainAliasByDefault(t *testing.T) { |
| 88 | fx, member := instanceFixture(t, "code-dom-alias-default") |
| 89 | fx.srv.cfg.AppsDomain = "oncarlos.test" |
| 90 | fx.srv.cfg.ReservePlatformDomains = true |
| 91 | code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), member, |
| 92 | attachBody("widget.oncarlos.test", "go.tito.io")) |
| 93 | if code != http.StatusBadRequest { |
| 94 | t.Fatalf("attach of an apps-domain alias with the flag unset = %d %s, want 400", code, out) |
| 95 | } |
| 96 | claimAbsent(t, fx, "widget.oncarlos.test") |
| 97 | } |
| 98 | |
| 99 | func TestAPIDomainAttachAllowsAppsDomainAlias(t *testing.T) { |
| 100 | fx, member := instanceFixture(t, "code-dom-alias-allow") |
| 101 | fx.srv.cfg.AppsDomain = "oncarlos.test" |
| 102 | fx.srv.cfg.ReservePlatformDomains = true |
| 103 | fx.srv.cfg.AppsDomainAliasAccounts = []string{fx.sqid} |
| 104 | |
| 105 | if got := attachDomain(t, fx, member, "widget.oncarlos.test", "go.tito.io"); !got.Created { |
| 106 | t.Fatalf("attach of a one-label apps-domain alias = %+v, want created true", got) |
| 107 | } |
| 108 | |
| 109 | for _, host := range []string{ |
| 110 | "a.widget.oncarlos.test", // nested |
| 111 | "oncarlos.test", // the apex itself |
| 112 | } { |
| 113 | code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), member, |
| 114 | attachBody(host, "go.tito.io")) |
| 115 | if code != http.StatusBadRequest { |
| 116 | t.Errorf("attach %q with aliases allowed = %d %s, want 400", host, code, out) |
| 117 | } |
| 118 | claimAbsent(t, fx, host) |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | func TestAPIDomainAttachAppsDomainAliasRejectsWildcardAndCatchall(t *testing.T) { |
| 123 | fx, member := instanceFixture(t, "code-dom-alias-wild") |
| 124 | fx.srv.cfg.AppsDomain = "oncarlos.test" |
| 125 | fx.srv.cfg.ReservePlatformDomains = true |
| 126 | fx.srv.cfg.AppsDomainAliasAccounts = []string{fx.sqid} |
| 127 | const host = "widget.oncarlos.test" |
| 128 | |
| 129 | for name, body := range map[string]string{ |
| 130 | "wildcard": `{"host":"` + host + `","route":"go.tito.io","wildcard":true}`, |
| 131 | "catchall": `{"host":"` + host + `","route":"go.tito.io","catchall":true}`, |
| 132 | } { |
| 133 | code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), member, body) |
| 134 | if code != http.StatusBadRequest { |
| 135 | t.Errorf("%s flag on an apps-domain alias = %d %s, want 400", name, code, out) |
| 136 | } |
| 137 | claimAbsent(t, fx, host) |
| 138 | } |
| 139 | if got := attachDomain(t, fx, member, host, "go.tito.io"); !got.Created { |
| 140 | t.Fatalf("plain attach of the same host with neither flag set = %+v, want created true", got) |
| 141 | } |
| 142 | } |
| 143 | |
| 144 | func TestAPIDomainAttachAppsDomainAliasStillRefusesForeignClaim(t *testing.T) { |
| 145 | fx, member := instanceFixture(t, "code-dom-alias-conflict") |
| 146 | fx.srv.cfg.AppsDomain = "oncarlos.test" |
| 147 | fx.srv.cfg.ReservePlatformDomains = true |
| 148 | fx.srv.cfg.AppsDomainAliasAccounts = []string{fx.sqid} |
| 149 | const host = "widget.oncarlos.test" |
| 150 | if _, _, err := domains.AttachClaim(context.Background(), fx.rel, domains.Claim{ |
| 151 | Host: host, Account: "cdef", App: "widgets", |
| 152 | Route: "widgets.example.net", CreatedAt: time.Now().UTC().Format(time.RFC3339), |
| 153 | }); err != nil { |
| 154 | t.Fatal(err) |
| 155 | } |
| 156 | code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), member, |
| 157 | attachBody(host, "go.tito.io")) |
| 158 | if code != http.StatusConflict { |
| 159 | t.Fatalf("attach of an apps-domain alias another account holds = %d %s, want 409", code, out) |
| 160 | } |
| 161 | if got := strings.TrimSpace(out); got != `{"error":"hostname unavailable"}` { |
| 162 | t.Fatalf("409 body = %s, want the neutral refusal", got) |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | func TestAPIDomainAttachAppsDomainAliasStillRefusesDeploymentRoute(t *testing.T) { |
| 167 | ctx := context.Background() |
| 168 | fx, member := instanceFixture(t, "code-dom-alias-route") |
| 169 | fx.srv.cfg.AppsDomain = "oncarlos.test" |
| 170 | fx.srv.cfg.ReservePlatformDomains = true |
| 171 | fx.srv.cfg.AppsDomainAliasAccounts = []string{fx.sqid} |
| 172 | const host = "widget.oncarlos.test" |
| 173 | if _, err := fx.rel.Put(ctx, edgesfile.Key("annie"), |
| 174 | []byte(`{"v":1,"box":"annie","eip":"198.51.100.9","hosts":{"`+host+`":{"role":"home"}}}`), ""); err != nil { |
| 175 | t.Fatal(err) |
| 176 | } |
| 177 | if err := status.WriteBox(ctx, fx.rel, status.Box{Label: "annie", GeneratedAt: time.Now()}); err != nil { |
| 178 | t.Fatal(err) |
| 179 | } |
| 180 | code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), member, |
| 181 | attachBody(host, "two.go.tito.io")) |
| 182 | if code != http.StatusConflict { |
| 183 | t.Fatalf("attach of an apps-domain alias a live box already serves = %d %s, want 409", code, out) |
| 184 | } |
| 185 | claimAbsent(t, fx, host) |
| 186 | } |
| 187 | |
| 188 | func TestAttachDomainCoreAppsDomainAlias(t *testing.T) { |
| 189 | fx, acct := attachFixture(t) |
| 190 | fx.srv.cfg.ReservePlatformDomains = true |
| 191 | fx.srv.cfg.Registry = attachRegistry(t, |
| 192 | registry.Route{Host: "story." + fx.sqid + ".oncarlos.com", Kind: "static", App: "story", |
| 193 | Account: fx.sqid, Channel: "canary/live", Status: "ready"}, |
| 194 | ) |
| 195 | const alias = "widget.oncarlos.com" |
| 196 | |
| 197 | if _, ref := fx.srv.attachDomain(context.Background(), acct, "story", alias, "", "", "paul@keymail.dev"); ref == nil || ref.Status != http.StatusUnprocessableEntity { |
| 198 | t.Fatalf("attach of an apps-domain alias with the flag unset = %+v, want 422", ref) |
| 199 | } |
| 200 | claimAbsent(t, fx, alias) |
| 201 | |
| 202 | fx.srv.cfg.AppsDomainAliasAccounts = []string{fx.sqid} |
| 203 | res, ref := fx.srv.attachDomain(context.Background(), acct, "story", alias, "", "", "paul@keymail.dev") |
| 204 | if ref != nil { |
| 205 | t.Fatalf("attach of an apps-domain alias with the flag set = refused %+v", ref) |
| 206 | } |
| 207 | want := "story." + fx.sqid + ".oncarlos.com" |
| 208 | if res.Claim.Route != want { |
| 209 | t.Fatalf("claim route = %q, want the ready route %q it aliases", res.Claim.Route, want) |
| 210 | } |
| 211 | |
| 212 | if _, ref := fx.srv.attachDomain(context.Background(), acct, "story", "a.b.oncarlos.com", "", "", "paul@keymail.dev"); ref == nil || ref.Status != http.StatusUnprocessableEntity { |
| 213 | t.Fatalf("attach of a nested apps-domain name with the flag set = %+v, want still 422", ref) |
| 214 | } |
| 215 | claimAbsent(t, fx, "a.b.oncarlos.com") |
| 216 | } |
| 217 | |
| 218 | func TestAttachDomainCoreAppsDomainAliasStillRefusesHeldHost(t *testing.T) { |
| 219 | fx, acct := attachFixture(t) |
| 220 | fx.srv.cfg.ReservePlatformDomains = true |
| 221 | fx.srv.cfg.AppsDomainAliasAccounts = []string{fx.sqid} |
| 222 | fx.srv.cfg.Registry = attachRegistry(t, |
| 223 | registry.Route{Host: "story." + fx.sqid + ".oncarlos.com", Kind: "static", App: "story", |
| 224 | Account: fx.sqid, Channel: "canary/live", Status: "ready"}, |
| 225 | ) |
| 226 | const alias = "widget.oncarlos.com" |
| 227 | seedClaim(t, fx, domains.Claim{Schema: 1, Host: alias, Account: "zzz", App: "rival"}) |
| 228 | |
| 229 | _, ref := fx.srv.attachDomain(context.Background(), acct, "story", alias, "", "", "paul@keymail.dev") |
| 230 | if ref == nil || ref.Status != http.StatusConflict || ref.Msg != "hostname unavailable" { |
| 231 | t.Fatalf("refusal = %+v, want exactly 409 hostname unavailable even though aliases are allowed", ref) |
| 232 | } |
| 233 | } |
| 234 | |
| 235 | func TestAppsDomainAliasAccountAuthority(t *testing.T) { |
| 236 | s := &Server{cfg: Config{ |
| 237 | AppsDomain: "platform.example.test", AppsDomainAliasAccounts: []string{"bap"}, |
| 238 | ReservePlatformDomains: true, |
| 239 | Origin: "https://console.platform.example.test", |
| 240 | DenyDomains: []string{"example.test", "platform.example.test", "blocked.platform.example.test"}, |
| 241 | }} |
| 242 | for _, tc := range []struct { |
| 243 | host, account string |
| 244 | denied bool |
| 245 | }{ |
| 246 | {"pullup.platform.example.test", "bap", false}, |
| 247 | {"pullup.platform.example.test", "bab", true}, |
| 248 | {"pullup.platform.example.test", "", true}, |
| 249 | {"bap.platform.example.test", "bap", true}, |
| 250 | {"bab.platform.example.test", "bap", true}, |
| 251 | {"edge.platform.example.test", "bap", true}, |
| 252 | {"console.platform.example.test", "bap", true}, |
| 253 | {"blocked.platform.example.test", "bap", true}, |
| 254 | {"x.bap.platform.example.test", "bap", true}, |
| 255 | {"platform.example.test", "bap", true}, |
| 256 | {"outside.example.test", "bap", true}, |
| 257 | } { |
| 258 | if got := s.domainHostDenied(tc.host, tc.account); got != tc.denied { |
| 259 | t.Errorf("%s/%s: denied=%v, want %v", tc.account, tc.host, got, tc.denied) |
| 260 | } |
| 261 | } |
| 262 | s.cfg.SteeringHost = "steering.platform.example.test." |
| 263 | if !s.domainHostDenied("steering.platform.example.test", "bap") { |
| 264 | t.Fatal("steering host allowed") |
| 265 | } |
| 266 | } |
| 267 | |
| 268 | func TestAppsDomainAliasCannotMintFirstRoute(t *testing.T) { |
| 269 | fx, acct := attachFixture(t) |
| 270 | fx.srv.cfg.ReservePlatformDomains = true |
| 271 | fx.srv.cfg.AppsDomainAliasAccounts = []string{fx.sqid} |
| 272 | fx.srv.cfg.Registry = attachRegistry(t) |
| 273 | for _, kind := range []string{"", "static"} { |
| 274 | _, ref := fx.srv.attachDomain(context.Background(), acct, "story", "pullup.oncarlos.com", kind, "", "actor") |
| 275 | if ref == nil || ref.Status != http.StatusUnprocessableEntity { |
| 276 | t.Fatalf("kind %q: refusal=%v", kind, ref) |
| 277 | } |
| 278 | claimAbsent(t, fx, "pullup.oncarlos.com") |
| 279 | } |
| 280 | } |
| 281 | |