carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "testing"
8 "time"
9)
10
11// The platform door (#209): a deployment's operator attaching a hostname in
12// the deployment's own zones (CONSOLE_DENY_DOMAINS — carloku.com on the
13// flagship, go.tito.io on Tito's) to an app, with members still refused.
14
15// platformFixture is instanceFixture on a deployment that reserves platform
16// domains, lists carloku.test as its own, and has a tunnel host INSIDE that
17// zone — the case where the deny list's two halves must not be confused.
18// paul owns the ops account; alice is an ordinary member of it.
19func platformFixture(t *testing.T, code string) (fx *consoleFixture, alice, paul string) {
20 t.Helper()
21 fx, alice = instanceFixture(t, code)
22 fx.srv.cfg.AppsDomain = "oncarlos.test"
23 fx.srv.cfg.ReservePlatformDomains = true
24 fx.srv.cfg.DenyDomains = []string{"carloku.test"}
25 fx.srv.cfg.TunnelHost = "hub.carloku.test"
26 return fx, alice, cliToken(t, fx, "paul@keymail.dev", code+"-paul")
27}
28
29func platformAttachBody(host, route string) string {
30 b, err := json.Marshal(map[string]any{"host": host, "route": route, "platform": true})
31 if err != nil {
32 panic(err)
33 }
34 return string(b)
35}
36
37func TestDomainHostPolicyMatrix(t *testing.T) {
38 s := &Server{cfg: Config{
39 AppsDomain: "apps.test",
40 ReservePlatformDomains: true,
41 Origin: "https://console.carloku.test",
42 RedirectHosts: []string{"retired.carloku.test"},
43 DenyDomains: []string{"carloku.test", "go.tito.test"},
44 TunnelHost: "hub.carloku.test",
45 }}
46 cases := []struct {
47 host string
48 want hostPolicy
49 }{
50 {"tickets.example.com", hostOpen},
51 {"carloku.test", hostPlatform},
52 {"www.carloku.test", hostPlatform},
53 {"go.tito.test", hostPlatform},
54 {"a.console.carloku.test", hostPlatform}, // under the console's own name
55 {"console.carloku.test", hostReserved}, // the console itself
56 {"retired.carloku.test", hostReserved}, // a retired console origin
57 {"hub.carloku.test", hostReserved}, // the tunnel host, though under a platform entry
58 {"x.hub.carloku.test", hostReserved},
59 {"edge.apps.test", hostReserved}, // the steering host
60 {"apps.test", hostReserved},
61 {"site.bab.apps.test", hostReserved}, // a tenant's address
62 {"web.sidecar.internal", hostReserved},
63 }
64 for _, c := range cases {
65 if got := s.domainHostPolicy(c.host, "bab"); got != c.want {
66 t.Errorf("domainHostPolicy(%q) = %v, want %v", c.host, got, c.want)
67 }
68 if got := s.domainHostDenied(c.host, "bab"); got != (c.want != hostOpen) {
69 t.Errorf("domainHostDenied(%q) = %v, want %v", c.host, got, c.want != hostOpen)
70 }
71 }
72 // Without ReservePlatformDomains the apps domain and the console's
73 // subtree are ordinary names, as before; the operator's own list and the
74 // tunnel host are not.
75 s.cfg.ReservePlatformDomains = false
76 for host, want := range map[string]hostPolicy{
77 "site.bab.apps.test": hostOpen,
78 "a.console.carloku.test": hostPlatform, // still under carloku.test
79 "carloku.test": hostPlatform,
80 "hub.carloku.test": hostReserved,
81 } {
82 if got := s.domainHostPolicy(host, "bab"); got != want {
83 t.Errorf("without reserve: domainHostPolicy(%q) = %v, want %v", host, got, want)
84 }
85 }
86}
87
88// TestPlatformAttachRefusesMembers: an ordinary member gets the old refusal
89// without the flag and a 403 with it, and no claim is written either way.
90func TestPlatformAttachRefusesMembers(t *testing.T) {
91 fx, alice, _ := platformFixture(t, "code-plat-member")
92 if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), alice,
93 attachBody("carloku.test", "go.tito.io")); code != http.StatusBadRequest {
94 t.Errorf("member attach without --platform = %d %s, want 400", code, out)
95 }
96 code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), alice,
97 platformAttachBody("carloku.test", "go.tito.io"))
98 if code != http.StatusForbidden {
99 t.Errorf("member attach with --platform = %d %s, want 403", code, out)
100 }
101 // The flag is refused on an ordinary hostname too: the answer depends on
102 // the caller alone, never on what the deny list holds.
103 if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), alice,
104 platformAttachBody("tickets.example.com", "go.tito.io")); code != http.StatusForbidden {
105 t.Errorf("member attach of an ordinary host with --platform = %d %s, want 403", code, out)
106 }
107 claimAbsent(t, fx, "carloku.test")
108 claimAbsent(t, fx, "tickets.example.com")
109}
110
111// TestPlatformAttachRefusesServices: an app token is never an operator, flag
112// or no flag, even one the ops owner minted (a service principal carries its
113// minter's address).
114func TestPlatformAttachRefusesServices(t *testing.T) {
115 fx, _, _ := platformFixture(t, "code-plat-svc")
116 tok, err := fx.st.NewAppToken(OpsAccountID, "titogo", "paul@keymail.dev", "signup", time.Now())
117 if err != nil {
118 t.Fatal(err)
119 }
120 code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), tok,
121 platformAttachBody("carloku.test", "go.tito.io"))
122 if code != http.StatusForbidden {
123 t.Errorf("app token attach with --platform = %d %s, want 403", code, out)
124 }
125 claimAbsent(t, fx, "carloku.test")
126}
127
128// TestPlatformAttachByOpsOwner: the ops owner needs the flag, and with it
129// gets an ordinary claim naming them as its author.
130func TestPlatformAttachByOpsOwner(t *testing.T) {
131 fx, _, paul := platformFixture(t, "code-plat-owner")
132 if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), paul,
133 attachBody("carloku.test", "go.tito.io")); code != http.StatusBadRequest {
134 t.Fatalf("owner attach WITHOUT --platform = %d %s, want 400", code, out)
135 }
136 claimAbsent(t, fx, "carloku.test")
137 for _, host := range []string{"carloku.test", "www.carloku.test"} {
138 code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), paul,
139 platformAttachBody(host, "go.tito.io"))
140 if code != http.StatusOK {
141 t.Fatalf("owner attach %s with --platform = %d %s, want 200", host, code, out)
142 }
143 c := claimOf(t, fx, host)
144 if c.Account != fx.sqid || c.App != "titogo" || c.Route != "go.tito.io" || c.CreatedBy != "paul@keymail.dev" {
145 t.Errorf("claim %s = %+v", host, c)
146 }
147 }
148 // An operator's flag on an ordinary hostname is harmless: it attaches
149 // exactly as it would without.
150 if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), paul,
151 platformAttachBody("tickets.example.com", "go.tito.io")); code != http.StatusOK {
152 t.Errorf("owner attach of an ordinary host with --platform = %d %s, want 200", code, out)
153 }
154}
155
156// TestPlatformAttachBySystemOperator: an address on CARLOS_SYSTEM_OPERATORS
157// may use the door on an account it is a member of, and only there; the ops
158// owner gets no reach into an account they are not in either.
159func TestPlatformAttachBySystemOperator(t *testing.T) {
160 fx, _, paul := platformFixture(t, "code-plat-sysop")
161 now := time.Now()
162 if err := fx.st.CreateAccount(42, "cdef", "carloku", "bob@keymail.dev", now); err != nil {
163 t.Fatal(err)
164 }
165 if _, err := fx.st.CreateAppIn(42, "website", "bob@keymail.dev", now); err != nil {
166 t.Fatal(err)
167 }
168 if err := fx.st.AddMember(42, "bill@keymail.dev", RoleMember, now); err != nil {
169 t.Fatal(err)
170 }
171 bill := cliToken(t, fx, "bill@keymail.dev", "code-plat-bill")
172
173 body := platformAttachBody("carloku.test", "website.cdef.oncarlos.test")
174 if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "cdef", "website"), bill, body); code != http.StatusForbidden {
175 t.Fatalf("bill before being listed = %d %s, want 403", code, out)
176 }
177 if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "cdef", "website"), paul, body); code != http.StatusNotFound {
178 t.Fatalf("ops owner on an account they are not in = %d %s, want 404", code, out)
179 }
180 claimAbsent(t, fx, "carloku.test")
181
182 fx.srv.cfg.SystemOperators = []string{"bill@keymail.dev"}
183 if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "cdef", "website"), bill, body); code != http.StatusOK {
184 t.Fatalf("listed system operator = %d %s, want 200", code, out)
185 }
186 if c := claimOf(t, fx, "carloku.test"); c.Account != "cdef" || c.App != "website" || c.CreatedBy != "bill@keymail.dev" {
187 t.Errorf("claim = %+v", c)
188 }
189}
190
191// TestPlatformAttachReservedStaysRefused: the door lifts the operator's own
192// list and nothing else. The console's names, the steering host, the tunnel
193// host and the tenant namespace stay refused to the operator too.
194func TestPlatformAttachReservedStaysRefused(t *testing.T) {
195 fx, _, paul := platformFixture(t, "code-plat-reserved")
196 fx.srv.cfg.Origin = "https://console.carloku.test"
197 // A deny entry naming the sidecar suffix must not make it a platform
198 // name the door lifts.
199 fx.srv.cfg.DenyDomains = append(fx.srv.cfg.DenyDomains, "sidecar.internal")
200 for _, host := range []string{
201 "console.carloku.test",
202 "hub.carloku.test",
203 "edge.oncarlos.test",
204 "oncarlos.test",
205 "titogo.bab.oncarlos.test",
206 "web.sidecar.internal",
207 } {
208 code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), paul,
209 platformAttachBody(host, "go.tito.io"))
210 if code != http.StatusBadRequest {
211 t.Errorf("operator attach of reserved %s = %d %s, want 400", host, code, out)
212 }
213 claimAbsent(t, fx, host)
214 }
215 // The shared core behind the static first-route branch must draw the
216 // same line on its own, with the door already granted.
217 acct, err := fx.st.AccountBySqid(fx.sqid)
218 if err != nil || acct == nil {
219 t.Fatalf("account: %v", err)
220 }
221 for _, host := range []string{"console.carloku.test", "hub.carloku.test", "edge.oncarlos.test", "titogo.bab.oncarlos.test"} {
222 if _, ref := fx.srv.attachDomainAs(context.Background(), acct, "titogo", host, "static", "", "paul@keymail.dev", true); ref == nil || ref.Status != http.StatusUnprocessableEntity {
223 t.Errorf("core attach of reserved %s with the door granted = %+v, want 422", host, ref)
224 }
225 claimAbsent(t, fx, host)
226 }
227}
228
229// TestPlatformClaimMembersCannotChangeIt: once the operator has attached a
230// platform hostname, the holding account's members cannot repoint it, flip
231// its capabilities, carve up its paths or release it. Only the operator can.
232func TestPlatformClaimMembersCannotChangeIt(t *testing.T) {
233 fx, alice, paul := platformFixture(t, "code-plat-hold")
234 const host = "carloku.test"
235 if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), paul,
236 platformAttachBody(host, "go.tito.io")); code != http.StatusOK {
237 t.Fatalf("operator attach = %d %s", code, out)
238 }
239 before := claimOf(t, fx, host)
240
241 refusals := []struct {
242 what, method, url, body string
243 }{
244 {"repoint", http.MethodPost, domainsURL(fx, "bab", "titogo"), attachBody(host, "two.go.tito.io")},
245 {"repoint with --platform", http.MethodPost, domainsURL(fx, "bab", "titogo"), platformAttachBody(host, "two.go.tito.io")},
246 {"catchall", http.MethodPost, domainsURL(fx, "bab", "titogo"), `{"host":"` + host + `","route":"go.tito.io","wildcard":true,"catchall":true}`},
247 {"paths", http.MethodPut, pathsURL(fx, "bab", "titogo", host), `{"paths":{"admin":"go.tito.io"}}`},
248 {"detach", http.MethodDelete, domainsURL(fx, "bab", "titogo") + "/" + host, ""},
249 }
250 for _, r := range refusals {
251 code, _, out := rawReq(t, r.method, r.url, alice, r.body)
252 if code != http.StatusBadRequest && code != http.StatusForbidden {
253 t.Errorf("member %s = %d %s, want a refusal", r.what, code, out)
254 }
255 }
256 after := claimOf(t, fx, host)
257 if after.Route != before.Route || after.Wildcard || after.Catchall || len(after.Paths) != 0 {
258 t.Fatalf("claim changed under member writes: %+v", after)
259 }
260
261 if code, _, out := rawReq(t, http.MethodPut, pathsURL(fx, "bab", "titogo", host), paul,
262 `{"paths":{"admin":"go.tito.io"}}`); code != http.StatusOK {
263 t.Errorf("operator paths = %d %s, want 200", code, out)
264 }
265 code, _, out := rawReq(t, http.MethodDelete, domainsURL(fx, "bab", "titogo")+"/"+host, paul, "")
266 if code != http.StatusOK {
267 t.Fatalf("operator detach = %d %s, want 200", code, out)
268 }
269 var got domainDetachResp
270 if err := json.Unmarshal([]byte(out), &got); err != nil || !got.Removed {
271 t.Fatalf("operator detach body %s (err %v), want removed", out, err)
272 }
273 claimAbsent(t, fx, host)
274}
275
276// TestPlatformBrowserDoors: the settings page offers no platform door. Its
277// attach refuses a platform hostname even to the operator, and its detach
278// lets the operator, and only the operator, release one.
279func TestPlatformBrowserDoors(t *testing.T) {
280 fx, _, paul := platformFixture(t, "code-plat-browser")
281 acct, err := fx.st.AccountBySqid(fx.sqid)
282 if err != nil || acct == nil {
283 t.Fatalf("account: %v", err)
284 }
285 ctx := context.Background()
286 if _, ref := fx.srv.attachDomain(ctx, acct, "titogo", "carloku.test", "", "", "paul@keymail.dev"); ref == nil || ref.Status != http.StatusUnprocessableEntity {
287 t.Fatalf("browser attach by the operator = %+v, want 422", ref)
288 }
289 if code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), paul,
290 platformAttachBody("carloku.test", "go.tito.io")); code != http.StatusOK {
291 t.Fatalf("operator attach = %d %s", code, out)
292 }
293 if ref := fx.srv.detachDomain(ctx, acct, "titogo", "carloku.test", "alice@keymail.dev"); ref == nil || ref.Status != http.StatusForbidden {
294 t.Fatalf("browser detach by a member = %+v, want 403", ref)
295 }
296 claimOf(t, fx, "carloku.test")
297 if ref := fx.srv.detachDomain(ctx, acct, "titogo", "carloku.test", "paul@keymail.dev"); ref != nil {
298 t.Fatalf("browser detach by the operator = %+v, want released", ref)
299 }
300 claimAbsent(t, fx, "carloku.test")
301}
302
303// TestAPIAttachRefusesSidecarNames: the API door's alias branch refuses a
304// sidecar name to a member too, with or without a deny entry covering it.
305func TestAPIAttachRefusesSidecarNames(t *testing.T) {
306 fx, alice, _ := platformFixture(t, "code-plat-sidecar")
307 code, _, out := rawReq(t, http.MethodPost, domainsURL(fx, "bab", "titogo"), alice,
308 attachBody("web.sidecar.internal", "go.tito.io"))
309 if code != http.StatusBadRequest {
310 t.Errorf("member alias attach of a sidecar name = %d %s, want 400", code, out)
311 }
312 claimAbsent(t, fx, "web.sidecar.internal")
313}
314