package console import ( "context" "encoding/json" "net/http" "testing" "time" ) // The platform door (#209): a deployment's operator attaching a hostname in // the deployment's own zones (CONSOLE_DENY_DOMAINS — carloku.com on the // flagship, go.tito.io on Tito's) to an app, with members still refused. // platformFixture is instanceFixture on a deployment that reserves platform // domains, lists carloku.test as its own, and has a tunnel host INSIDE that // zone — the case where the deny list's two halves must not be confused. // paul owns the ops account; alice is an ordinary member of it. func platformFixture(t *testing.T, code string) (fx *consoleFixture, alice, paul string) { t.Helper() fx, alice = instanceFixture(t, code) fx.srv.cfg.AppsDomain = "oncarlos.test" fx.srv.cfg.ReservePlatformDomains = true fx.srv.cfg.DenyDomains = []string{"carloku.test"} fx.srv.cfg.TunnelHost = "hub.carloku.test" return fx, alice, cliToken(t, fx, "paul@keymail.dev", code+"-paul") } func platformAttachBody(host, route string) string { b, err := json.Marshal(map[string]any{"host": host, "route": route, "platform": true}) if err != nil { panic(err) } return string(b) } func TestDomainHostPolicyMatrix(t *testing.T) { s := &Server{cfg: Config{ AppsDomain: "apps.test", ReservePlatformDomains: true, Origin: "https://console.carloku.test", RedirectHosts: []string{"retired.carloku.test"}, DenyDomains: []string{"carloku.test", "go.tito.test"}, TunnelHost: "hub.carloku.test", }} cases := []struct { host string want hostPolicy }{ {"tickets.example.com", hostOpen}, {"carloku.test", hostPlatform}, {"www.carloku.test", hostPlatform}, {"go.tito.test", hostPlatform}, {"a.console.carloku.test", hostPlatform}, // under the console's own name {"console.carloku.test", hostReserved}, // the console itself {"retired.carloku.test", hostReserved}, // a retired console origin {"hub.carloku.test", hostReserved}, // the tunnel host, though under a platform entry {"x.hub.carloku.test", hostReserved}, {"edge.apps.test", hostReserved}, // the steering host {"apps.test", hostReserved}, {"site.bab.apps.test", hostReserved}, // a tenant's address {"web.sidecar.internal", hostReserved}, } for _, c := range cases { if got := s.domainHostPolicy(c.host, "bab"); got != c.want { t.Errorf("domainHostPolicy(%q) = %v, want %v", c.host, got, c.want) } if got := s.domainHostDenied(c.host, "bab"); got != (c.want != hostOpen) { t.Errorf("domainHostDenied(%q) = %v, want %v", c.host, got, c.want != hostOpen) } } // Without ReservePlatformDomains the apps domain and the console's // subtree are ordinary names, as before; the operator's own list and the // tunnel host are not. s.cfg.ReservePlatformDomains = false for host, want := range map[string]hostPolicy{ "site.bab.apps.test": hostOpen, "a.console.carloku.test": hostPlatform, // still under carloku.test "carloku.test": hostPlatform, "hub.carloku.test": hostReserved, } { if got := s.domainHostPolicy(host, "bab"); got != want { t.Errorf("without reserve: domainHostPolicy(%q) = %v, want %v", host, got, want) } } } // TestPlatformAttachRefusesMembers: an ordinary member gets the old refusal // without the flag and a 403 with it, and no claim is written either way. func TestPlatformAttachRefusesMembers(t *testing.T) { fx, alice, _ := platformFixture(t, "code-plat-member") if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), alice, attachBody("carloku.test", "go.tito.io")); code != http.StatusBadRequest { t.Errorf("member attach without --platform = %d %s, want 400", code, out) } code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), alice, platformAttachBody("carloku.test", "go.tito.io")) if code != http.StatusForbidden { t.Errorf("member attach with --platform = %d %s, want 403", code, out) } // The flag is refused on an ordinary hostname too: the answer depends on // the caller alone, never on what the deny list holds. if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), alice, platformAttachBody("tickets.example.com", "go.tito.io")); code != http.StatusForbidden { t.Errorf("member attach of an ordinary host with --platform = %d %s, want 403", code, out) } claimAbsent(t, fx, "carloku.test") claimAbsent(t, fx, "tickets.example.com") } // TestPlatformAttachRefusesServices: an app token is never an operator, flag // or no flag, even one the ops owner minted (a service principal carries its // minter's address). func TestPlatformAttachRefusesServices(t *testing.T) { fx, _, _ := platformFixture(t, "code-plat-svc") tok, err := fx.st.NewAppToken(OpsAccountID, "titogo", "paul@keymail.dev", "signup", time.Now()) if err != nil { t.Fatal(err) } code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), tok, platformAttachBody("carloku.test", "go.tito.io")) if code != http.StatusForbidden { t.Errorf("app token attach with --platform = %d %s, want 403", code, out) } claimAbsent(t, fx, "carloku.test") } // TestPlatformAttachByOpsOwner: the ops owner needs the flag, and with it // gets an ordinary claim naming them as its author. func TestPlatformAttachByOpsOwner(t *testing.T) { fx, _, paul := platformFixture(t, "code-plat-owner") if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), paul, attachBody("carloku.test", "go.tito.io")); code != http.StatusBadRequest { t.Fatalf("owner attach WITHOUT --platform = %d %s, want 400", code, out) } claimAbsent(t, fx, "carloku.test") for _, host := range []string{"carloku.test", "www.carloku.test"} { code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), paul, platformAttachBody(host, "go.tito.io")) if code != http.StatusOK { t.Fatalf("owner attach %s with --platform = %d %s, want 200", host, code, out) } c := claimOf(t, fx, host) if c.Account != fx.sqid || c.App != "titogo" || c.Route != "go.tito.io" || c.CreatedBy != "paul@keymail.dev" { t.Errorf("claim %s = %+v", host, c) } } // An operator's flag on an ordinary hostname is harmless: it attaches // exactly as it would without. if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), paul, platformAttachBody("tickets.example.com", "go.tito.io")); code != http.StatusOK { t.Errorf("owner attach of an ordinary host with --platform = %d %s, want 200", code, out) } } // TestPlatformAttachBySystemOperator: an address on CARLOS_SYSTEM_OPERATORS // may use the door on an account it is a member of, and only there; the ops // owner gets no reach into an account they are not in either. func TestPlatformAttachBySystemOperator(t *testing.T) { fx, _, paul := platformFixture(t, "code-plat-sysop") now := time.Now() if err := fx.st.CreateAccount(42, "cdef", "carloku", "bob@keymail.dev", now); err != nil { t.Fatal(err) } if _, err := fx.st.CreateAppIn(42, "website", "bob@keymail.dev", now); err != nil { t.Fatal(err) } if err := fx.st.AddMember(42, "bill@keymail.dev", RoleMember, now); err != nil { t.Fatal(err) } bill := cliToken(t, fx, "bill@keymail.dev", "code-plat-bill") body := platformAttachBody("carloku.test", "website.cdef.oncarlos.test") if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "cdef", "website"), bill, body); code != http.StatusForbidden { t.Fatalf("bill before being listed = %d %s, want 403", code, out) } if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "cdef", "website"), paul, body); code != http.StatusNotFound { t.Fatalf("ops owner on an account they are not in = %d %s, want 404", code, out) } claimAbsent(t, fx, "carloku.test") fx.srv.cfg.SystemOperators = []string{"bill@keymail.dev"} if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "cdef", "website"), bill, body); code != http.StatusOK { t.Fatalf("listed system operator = %d %s, want 200", code, out) } if c := claimOf(t, fx, "carloku.test"); c.Account != "cdef" || c.App != "website" || c.CreatedBy != "bill@keymail.dev" { t.Errorf("claim = %+v", c) } } // TestPlatformAttachReservedStaysRefused: the door lifts the operator's own // list and nothing else. The console's names, the steering host, the tunnel // host and the tenant namespace stay refused to the operator too. func TestPlatformAttachReservedStaysRefused(t *testing.T) { fx, _, paul := platformFixture(t, "code-plat-reserved") fx.srv.cfg.Origin = "https://console.carloku.test" // A deny entry naming the sidecar suffix must not make it a platform // name the door lifts. fx.srv.cfg.DenyDomains = append(fx.srv.cfg.DenyDomains, "sidecar.internal") for _, host := range []string{ "console.carloku.test", "hub.carloku.test", "edge.oncarlos.test", "oncarlos.test", "titogo.bab.oncarlos.test", "web.sidecar.internal", } { code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), paul, platformAttachBody(host, "go.tito.io")) if code != http.StatusBadRequest { t.Errorf("operator attach of reserved %s = %d %s, want 400", host, code, out) } claimAbsent(t, fx, host) } // The shared core behind the static first-route branch must draw the // same line on its own, with the door already granted. acct, err := fx.st.AccountBySqid(fx.sqid) if err != nil || acct == nil { t.Fatalf("account: %v", err) } for _, host := range []string{"console.carloku.test", "hub.carloku.test", "edge.oncarlos.test", "titogo.bab.oncarlos.test"} { if _, ref := fx.srv.attachDomainAs(context.Background(), acct, "titogo", host, "static", "", "paul@keymail.dev", true); ref == nil || ref.Status != http.StatusUnprocessableEntity { t.Errorf("core attach of reserved %s with the door granted = %+v, want 422", host, ref) } claimAbsent(t, fx, host) } } // TestPlatformClaimMembersCannotChangeIt: once the operator has attached a // platform hostname, the holding account's members cannot repoint it, flip // its capabilities, carve up its paths or release it. Only the operator can. func TestPlatformClaimMembersCannotChangeIt(t *testing.T) { fx, alice, paul := platformFixture(t, "code-plat-hold") const host = "carloku.test" if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), paul, platformAttachBody(host, "go.tito.io")); code != http.StatusOK { t.Fatalf("operator attach = %d %s", code, out) } before := claimOf(t, fx, host) refusals := []struct { what, method, url, body string }{ {"repoint", http.MethodPost, domainsURL(fx, "bab", "titogo"), attachBody(host, "two.go.tito.io")}, {"repoint with --platform", http.MethodPost, domainsURL(fx, "bab", "titogo"), platformAttachBody(host, "two.go.tito.io")}, {"catchall", http.MethodPost, domainsURL(fx, "bab", "titogo"), `{"host":"` + host + `","route":"go.tito.io","wildcard":true,"catchall":true}`}, {"paths", http.MethodPut, pathsURL(fx, "bab", "titogo", host), `{"paths":{"admin":"go.tito.io"}}`}, {"detach", http.MethodDelete, domainsURL(fx, "bab", "titogo") + "/" + host, ""}, } for _, r := range refusals { code, _, out := rawReq(t, r.method, r.url, alice, r.body) if code != http.StatusBadRequest && code != http.StatusForbidden { t.Errorf("member %s = %d %s, want a refusal", r.what, code, out) } } after := claimOf(t, fx, host) if after.Route != before.Route || after.Wildcard || after.Catchall || len(after.Paths) != 0 { t.Fatalf("claim changed under member writes: %+v", after) } if code, _, out := rawReq(t, http.MethodPut, pathsURL(fx, "bab", "titogo", host), paul, `{"paths":{"admin":"go.tito.io"}}`); code != http.StatusOK { t.Errorf("operator paths = %d %s, want 200", code, out) } code, _, out := rawReq(t, http.MethodDelete, domainsURL(fx, "bab", "titogo")+"/"+host, paul, "") if code != http.StatusOK { t.Fatalf("operator detach = %d %s, want 200", code, out) } var got domainDetachResp if err := json.Unmarshal([]byte(out), &got); err != nil || !got.Removed { t.Fatalf("operator detach body %s (err %v), want removed", out, err) } claimAbsent(t, fx, host) } // TestPlatformBrowserDoors: the settings page offers no platform door. Its // attach refuses a platform hostname even to the operator, and its detach // lets the operator, and only the operator, release one. func TestPlatformBrowserDoors(t *testing.T) { fx, _, paul := platformFixture(t, "code-plat-browser") acct, err := fx.st.AccountBySqid(fx.sqid) if err != nil || acct == nil { t.Fatalf("account: %v", err) } ctx := context.Background() if _, ref := fx.srv.attachDomain(ctx, acct, "titogo", "carloku.test", "", "", "paul@keymail.dev"); ref == nil || ref.Status != http.StatusUnprocessableEntity { t.Fatalf("browser attach by the operator = %+v, want 422", ref) } if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), paul, platformAttachBody("carloku.test", "go.tito.io")); code != http.StatusOK { t.Fatalf("operator attach = %d %s", code, out) } if ref := fx.srv.detachDomain(ctx, acct, "titogo", "carloku.test", "alice@keymail.dev"); ref == nil || ref.Status != http.StatusForbidden { t.Fatalf("browser detach by a member = %+v, want 403", ref) } claimOf(t, fx, "carloku.test") if ref := fx.srv.detachDomain(ctx, acct, "titogo", "carloku.test", "paul@keymail.dev"); ref != nil { t.Fatalf("browser detach by the operator = %+v, want released", ref) } claimAbsent(t, fx, "carloku.test") } // TestAPIAttachRefusesSidecarNames: the API door's alias branch refuses a // sidecar name to a member too, with or without a deny entry covering it. func TestAPIAttachRefusesSidecarNames(t *testing.T) { fx, alice, _ := platformFixture(t, "code-plat-sidecar") code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), alice, attachBody("web.sidecar.internal", "go.tito.io")) if code != http.StatusBadRequest { t.Errorf("member alias attach of a sidecar name = %d %s, want 400", code, out) } claimAbsent(t, fx, "web.sidecar.internal") }