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 "net/http"
6 "net/url"
7 "strings"
8 "testing"
9
10 "github.com/carlosframework/platform/internal/provision"
11)
12
13func TestAppSettingsGateSetAndClear(t *testing.T) {
14 fx, token := instanceFixture(t, "code-settings-gate")
15 letStateUpstreams(t, fx, "alice@keymail.dev")
16 enableInstances(t, fx, "titogo", sharedPoolManifest())
17 if status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), token,
18 `{"host":"pw.go.tito.io","region":"ie","addrs":["127.0.0.1:7001"]}`); status != http.StatusCreated {
19 t.Fatalf("create = %d %s", status, out)
20 }
21 c := newBrowser(t)
22 signUp(t, fx, c, "code-settings-gate-browser")
23 csrf := sessionToken(t, fx, c)
24
25 // The page shows the host as open, with a form.
26 _, _, body := get(t, c, fx.u("/apps/titogo/settings/domains"))
27 if !strings.Contains(body, "Password gate") || !strings.Contains(body, "pw.go.tito.io") || !strings.Contains(body, "Open") {
28 t.Fatalf("settings page lacks the gate section: %s", body)
29 }
30
31 status, loc, _ := postForm(t, c, fx.u("/apps/titogo/gate"),
32 url.Values{"csrf": {csrf}, "host": {"pw.go.tito.io"}, "action": {"set"}, "password": {"correct horse"}})
33 if status != http.StatusSeeOther || loc != fx.path("/apps/titogo/settings/domains") {
34 t.Fatalf("set = %d -> %q", status, loc)
35 }
36 rec, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "pw.go.tito.io")
37 if err != nil {
38 t.Fatal(err)
39 }
40 if !strings.HasPrefix(rec.Gate, "$2a$10$") {
41 t.Fatalf("stored gate = %q", rec.Gate)
42 }
43 _, _, body = get(t, c, fx.u("/apps/titogo/settings/domains"))
44 if !strings.Contains(body, "Password set on pw.go.tito.io.") || !strings.Contains(body, "Turn off") {
45 t.Fatalf("after set: %s", body)
46 }
47 if strings.Contains(body, "$2a$") || strings.Contains(body, "correct horse") {
48 t.Fatal("page leaks the hash or the password")
49 }
50
51 // A short password is refused with the rule, and nothing changes.
52 postForm(t, c, fx.u("/apps/titogo/gate"),
53 url.Values{"csrf": {csrf}, "host": {"pw.go.tito.io"}, "action": {"set"}, "password": {"short"}})
54 _, _, body = get(t, c, fx.u("/apps/titogo/settings/domains"))
55 if !strings.Contains(body, "at least 8 characters") {
56 t.Fatalf("short password not refused on the page: %s", body)
57 }
58
59 status, _, _ = postForm(t, c, fx.u("/apps/titogo/gate"),
60 url.Values{"csrf": {csrf}, "host": {"pw.go.tito.io"}, "action": {"clear"}})
61 if status != http.StatusSeeOther {
62 t.Fatalf("clear = %d", status)
63 }
64 rec, _ = provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "pw.go.tito.io")
65 if rec.Gate != "" {
66 t.Fatalf("gate after clear = %q", rec.Gate)
67 }
68 events, _ := fx.st.Events(OpsAccountID, "titogo", 20)
69 var saw int
70 for _, e := range events {
71 if e.Action == "gate" {
72 saw++
73 if strings.Contains(e.Detail, "correct horse") {
74 t.Fatal("audit carries the password")
75 }
76 }
77 }
78 if saw != 2 {
79 t.Fatalf("gate audit rows = %d, want 2", saw)
80 }
81}
82
83// The gate door is not an oracle. A signed-in browser that belongs to no
84// account here gets the account family's 404 — byte for byte what an
85// account that does not exist answers, so a stranger cannot use the gate
86// form to learn that this account, this app or this host is real — and the
87// record is untouched. Worth pinning on THIS door in particular: it is the
88// one that takes a password, and a stranger who could set one would lock an
89// account out of its own site.
90func TestAppSettingsGateIsNotAnOracle(t *testing.T) {
91 fx, token := instanceFixture(t, "code-settings-gate-stranger")
92 letStateUpstreams(t, fx, "alice@keymail.dev")
93 enableInstances(t, fx, "titogo", sharedPoolManifest())
94 if status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), token,
95 `{"host":"pw.go.tito.io","region":"ie","addrs":["127.0.0.1:7001"]}`); status != http.StatusCreated {
96 t.Fatalf("create = %d %s", status, out)
97 }
98
99 stranger := newBrowser(t)
100 fx.setUser("eve@keymail.dev") // signs up into an account of her own
101 signUp(t, fx, stranger, "code-settings-gate-stranger-browser")
102 csrf := sessionToken(t, fx, stranger)
103
104 form := url.Values{"csrf": {csrf}, "host": {"pw.go.tito.io"}, "action": {"set"}, "password": {"eve was here"}}
105 ghost, _, missing := postForm(t, stranger, fx.ts.URL+"/a/zzzzz/apps/titogo/gate", form)
106 if ghost != http.StatusNotFound {
107 t.Fatalf("the yardstick is not a 404: nonexistent account = %d", ghost)
108 }
109 status, loc, body := postForm(t, stranger, fx.u("/apps/titogo/gate"), form)
110 if status != http.StatusNotFound {
111 t.Fatalf("gate POST as a non-member = %d -> %q, want 404", status, loc)
112 }
113 if body != missing {
114 t.Fatalf("a non-member's 404 differs from a nonexistent account's:\n%q\nvs\n%q", body, missing)
115 }
116 rec, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "pw.go.tito.io")
117 if err != nil {
118 t.Fatal(err)
119 }
120 if rec.Gate != "" {
121 t.Fatalf("a stranger set a gate: %q", rec.Gate)
122 }
123}
124
125func TestAppSettingsGateNeedsCSRF(t *testing.T) {
126 fx, _ := instanceFixture(t, "code-settings-gate-csrf")
127 c := newBrowser(t)
128 signUp(t, fx, c, "code-settings-gate-csrf-browser")
129 status, _, body := postForm(t, c, fx.u("/apps/titogo/gate"),
130 url.Values{"csrf": {"not-the-token"}, "host": {"pw.go.tito.io"}, "action": {"clear"}})
131 if status != http.StatusForbidden || !strings.Contains(body, "Cross-site request blocked.") {
132 t.Fatalf("got %d %s", status, body)
133 }
134}
135