oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1package home
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "strings"
7 "testing"
8)
9
10func newMountedChooser(t *testing.T) chooserFixture {
11 t.Helper()
12 f := newChooserFixture(t)
13 f.handler = f.p.directory.Router()
14 return f
15}
16
17func TestDirectoryRuntimeMountUsesCanonicalRegistryAndCurrentMembership(t *testing.T) {
18 f := newMountedChooser(t)
19 cookie := f.session(t, chooserPerson)
20 r := f.request("GET", docsEntryPath, "", "", cookie)
21 if r.Code != 200 || !strings.Contains(r.Body.String(), "This account has no team with Docs available.") {
22 t.Fatal(r.Code, r.Body.String())
23 }
24 noStore(t, r)
25 id := f.team(t, "A1", "alpha", "Alpha", chooserPerson)
26 f.team(t, "B2", "beta", "Foreign team", chooserOther)
27 r = f.request("GET", docsEntryPath, "", "", cookie)
28 if r.Code != 303 || r.Header().Get("Location") != "https://alpha.docs.oficina.works/" {
29 t.Fatal(r.Code, r.Header())
30 }
31 noStore(t, r)
32 for _, origin := range []string{"https://alpha.docs.oficina.works.evil.example", "https://evil-alpha.docs.oficina.works", "https://gamma.docs.oficina.works", "https://ALPHA.docs.oficina.works", "https://alpha.docs.oficina.works:443", "https://alpha.docs.oficina.works/path", "https://person@alpha.docs.oficina.works", "https://overnight-docs.bds.oncarlos.com"} {
33 if _, err := f.p.directory.db.Writer().Exec(`UPDATE team_instances SET origin=? WHERE team_id=?`, origin, id); err != nil {
34 t.Fatal(err)
35 }
36 r = f.request("GET", docsEntryPath, "", "", cookie)
37 if r.Code != 503 || r.Header().Get("Location") != "" || strings.Contains(r.Body.String(), origin) || strings.Contains(r.Body.String(), "Foreign team") {
38 t.Fatal("unsafe stored destination", origin, r.Code)
39 }
40 noStore(t, r)
41 }
42 if _, err := f.p.directory.db.Writer().Exec(`UPDATE team_instances SET origin='https://alpha.docs.oficina.works' WHERE team_id=?`, id); err != nil {
43 t.Fatal(err)
44 }
45 if _, err := f.p.directory.db.Writer().Exec(`DELETE FROM memberships WHERE team_id=? AND person_ref=?`, id, chooserPerson); err != nil {
46 t.Fatal(err)
47 }
48 r = f.request("GET", docsEntryPath, "", "", cookie)
49 if r.Code != 200 || strings.Contains(r.Body.String(), "Foreign team") || r.Header().Get("Location") != "" {
50 t.Fatal(r.Code, r.Body.String())
51 }
52}
53
54func TestDirectoryRuntimeNoStoreAndCookiesPrecedeRouterRefusals(t *testing.T) {
55 f := newMountedChooser(t)
56 cookie := f.session(t, chooserPerson)
57 f.team(t, "A1", "alpha", "Private team", chooserPerson)
58 cases := []struct {
59 method, path, host, origin, fetch string
60 cookies []*http.Cookie
61 want int
62 }{
63 {"GET", docsEntryPath, "evil.example", "", "", nil, 404},
64 {"DELETE", docsEntryPath, "", "", "", nil, 405},
65 {"GET", docsChoicePath, "", "", "", nil, 405},
66 {"GET", docsEntryPath + "?return_to=https://evil.example", "", "", "", nil, 404},
67 {"POST", docsChoicePath, "", "", "", []*http.Cookie{cookie}, 403},
68 {"POST", docsChoicePath, "", Origin, "", []*http.Cookie{cookie}, 403},
69 {"POST", docsChoicePath, "", directoryOrigin, "same-site", []*http.Cookie{cookie}, 403},
70 {"GET", docsEntryPath, "", "", "", []*http.Cookie{cookie, cookie}, 400},
71 {"POST", docsChoicePath, "", directoryOrigin, "", []*http.Cookie{cookie, {Name: cookie.Name, Value: "different"}}, 400},
72 }
73 for _, tc := range cases {
74 req := httptest.NewRequest(tc.method, directoryOrigin+tc.path, strings.NewReader("team=A1"))
75 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
76 if tc.host != "" {
77 req.Host = tc.host
78 }
79 if tc.origin != "" {
80 req.Header.Set("Origin", tc.origin)
81 }
82 if tc.fetch != "" {
83 req.Header.Set("Sec-Fetch-Site", tc.fetch)
84 }
85 for _, c := range tc.cookies {
86 req.AddCookie(c)
87 }
88 rec := httptest.NewRecorder()
89 f.handler.ServeHTTP(rec, req)
90 if rec.Code != tc.want || rec.Header().Get("Cache-Control") != "no-store" || strings.Contains(rec.Body.String(), "Private team") {
91 t.Fatalf("%s %s host=%s: %d %v", tc.method, tc.path, tc.host, rec.Code, rec.Header())
92 }
93 if len(rec.Result().Cookies()) != 0 {
94 t.Fatal("refusal changed cookies")
95 }
96 }
97}
98
99func TestDirectoryRuntimeAnonymousReturnAndRoleSchemaSeparation(t *testing.T) {
100 f := newMountedChooser(t)
101 r := f.request("GET", docsEntryPath, "", "", nil)
102 if r.Code != 303 || r.Header().Get("Location") != "/auth/start?return_to=%2Fapps%2Fdocs" {
103 t.Fatal(r.Code, r.Header())
104 }
105 noStore(t, r)
106 for _, tc := range []struct {
107 app *App
108 want int
109 }{{f.p.directory, 3}, {f.p.credential.App, 0}, {newTestApp(t).App, 0}} {
110 var count int
111 if err := tc.app.db.Writer().QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN ('teams','team_instances','memberships')`).Scan(&count); err != nil {
112 t.Fatal(err)
113 }
114 if count != tc.want {
115 t.Fatal("directory schema crossed roles", count, tc.want)
116 }
117 if tc.want == 0 {
118 rec := exchangeRequest(tc.app, "GET", directoryOrigin+docsEntryPath, "", nil)
119 if rec.Code != 404 {
120 t.Fatal("chooser reachable in other role", rec.Code)
121 }
122 }
123 }
124 apps := directoryApps()
125 if len(apps) != 6 || apps[0].slug != "docs" || !apps[0].integrated || apps[0].provisionable || apps[0].foreignOrigin != "" {
126 t.Fatal("registry advertised unsupported provisioning")
127 }
128 for _, app := range apps {
129 if app.provisionable || app.foreignOrigin != "" {
130 t.Fatal("entry enabled provisioning")
131 }
132 }
133 apps[0].slug = "changed"
134 if directoryApps()[0].slug != "docs" {
135 t.Fatal("registry shared mutable backing")
136 }
137}
138
139func TestDirectoryRuntimeExpiredChoiceReturnsToFreshTeamSelection(t *testing.T) {
140 f := newMountedChooser(t)
141 cookie := f.session(t, chooserPerson)
142 f.team(t, "A1", "alpha", "Alpha", chooserPerson)
143 f.team(t, "B2", "beta", "Beta", chooserPerson)
144 for _, metadata := range []string{"same-origin", "none"} {
145 req := httptest.NewRequest("POST", directoryOrigin+docsChoicePath, strings.NewReader("team=B2"))
146 req.AddCookie(cookie)
147 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
148 req.Header.Set("Sec-Fetch-Site", metadata)
149 rec := httptest.NewRecorder()
150 f.handler.ServeHTTP(rec, req)
151 assertDocsContinuation(t, rec, "https://beta.docs.oficina.works/")
152 }
153 if _, err := f.p.directory.db.Writer().Exec(`UPDATE sessions SET expires_at='2000-01-01T00:00:00Z'`); err != nil {
154 t.Fatal(err)
155 }
156 for _, c := range []*http.Cookie{cookie, nil} {
157 r := f.request("POST", docsChoicePath, directoryOrigin, "team=SecretSubmittedTeamXYZ&return_to=https%3A%2F%2Fevil.example%2Fsecret-navigation", c)
158 cleared := ""
159 if c != nil {
160 cleared = c.Name
161 }
162 assertChooserSigninContinuation(t, r, cleared)
163 for _, forbidden := range []string{"SecretSubmittedTeamXYZ", "evil.example", "secret-navigation"} {
164 if strings.Contains(r.Body.String(), forbidden) {
165 t.Fatal("submitted selection reflected")
166 }
167 }
168 }
169 r := f.request("GET", docsEntryPath, "", "", cookie)
170 if r.Code != 303 || r.Header().Get("Location") != "/auth/start?return_to=%2Fapps%2Fdocs" {
171 t.Fatal("anonymous GET changed", r.Code, r.Header())
172 }
173 invalid := f.session(t, "email@example.invalid")
174 for _, method := range []string{"GET", "POST"} {
175 path := docsEntryPath
176 if method == "POST" {
177 path = docsChoicePath
178 }
179 r = f.request(method, path, directoryOrigin, "team=A1", invalid)
180 if r.Code != 404 || strings.Contains(r.Body.String(), "http-equiv") {
181 t.Fatal("invalid subject entered authentication continuation", r.Code)
182 }
183 noStore(t, r)
184 }
185}
186
187func assertChooserSigninContinuation(t *testing.T, r *httptest.ResponseRecorder, cleared string) {
188 t.Helper()
189 cookies := r.Result().Cookies()
190 if cleared == "" {
191 if len(cookies) != 0 {
192 t.Fatal("anonymous continuation minted state")
193 }
194 } else {
195 if len(cookies) != 1 {
196 t.Fatal("expired continuation did not only clear its session")
197 }
198 c := cookies[0]
199 if c.Name != cleared || c.Value != "" || c.MaxAge >= 0 || c.Path != "/" || !c.Secure || !c.HttpOnly || c.SameSite != http.SameSiteLaxMode {
200 t.Fatal("expired continuation replaced session state")
201 }
202 }
203 assertContinuationPage(t, r, directoryOrigin+"/auth/start?return_to=%2Fapps%2Fdocs")
204}
205