~bigbes/sr-ht-dolt

ref: bee205061dd2fcfc291046ffad18878ccfcacf5f sr-ht-dolt/authn/cookie_test.go -rw-r--r-- 5.6 KiB
bee20506 — Eugene Blikh web: tell a database outage from a missing database 5 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package authn

import (
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"sourcecraft.dev/bigbes/sr-ht-core/auth"

	"sourcecraft.dev/bigbes/sr-ht-ecore/login"
)

// captureHandler records the caller present on the request context when reached.
func captureHandler(dst **auth.AuthContext, reached *bool) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		*reached = true
		*dst = CallerFromContext(r.Context())
		w.WriteHeader(http.StatusOK)
	})
}

func runCookieMiddleware(t *testing.T, cookie *http.Cookie) (*auth.AuthContext, int) {
	t.Helper()
	var got *auth.AuthContext
	var reached bool
	h := OptionalCookieMiddleware()(captureHandler(&got, &reached))

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	if cookie != nil {
		req.AddCookie(cookie)
	}
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	require.True(t, reached, "middleware must always call next (never rejects)")
	return got, rec.Code
}

func TestOptionalCookieMiddleware_ValidCookie(t *testing.T) {
	withStubBackend(t, &stubBackend{users: map[string]auth.AuthContext{
		"bigbes": sampleUser(1, "bigbes", auth.USER_TYPE_USER),
	}})

	got, code := runCookieMiddleware(t, &http.Cookie{
		Name:  login.CookieName,
		Value: forgeCookie(t, "bigbes"),
	})
	require.Equal(t, http.StatusOK, code)
	require.NotNil(t, got, "expected an authenticated caller")
	assert.Equal(t, "bigbes", got.Username)
	assert.Equal(t, 1, got.UserID)
	assert.Equal(t, auth.AUTH_COOKIE, got.AuthMethod)
}

// TestOptionalCookieMiddleware_StripsCanonicalSigil pins a difference from the
// decode this package used to carry: a payload holding the canonical "~name"
// form resolves to the bare name rather than being looked up with the sigil
// still on it — which meta would answer for nobody.
func TestOptionalCookieMiddleware_StripsCanonicalSigil(t *testing.T) {
	stub := &stubBackend{users: map[string]auth.AuthContext{
		"bigbes": sampleUser(1, "bigbes", auth.USER_TYPE_USER),
	}}
	withStubBackend(t, stub)

	got, code := runCookieMiddleware(t, &http.Cookie{
		Name:  login.CookieName,
		Value: forgeCookie(t, "~bigbes"),
	})
	require.Equal(t, http.StatusOK, code)
	require.NotNil(t, got)
	assert.Equal(t, "bigbes", got.Username)
	// The backend is asked for the bare name, not "~bigbes". Asserting on the
	// resolved caller alone would not show this: the stub normalises the sigil
	// away itself, as meta.LookupUser does, so both spellings resolve either
	// way and only the recorded argument says which one was sent.
	assert.Equal(t, []string{"bigbes"}, stub.lookedUp)
}

func TestOptionalCookieMiddleware_SuspendedStillResolves(t *testing.T) {
	withStubBackend(t, &stubBackend{users: map[string]auth.AuthContext{
		"susp": sampleUser(2, "susp", auth.USER_TYPE_SUSPENDED),
	}})
	got, _ := runCookieMiddleware(t, &http.Cookie{
		Name:  login.CookieName,
		Value: forgeCookie(t, "susp"),
	})
	require.NotNil(t, got, "suspended users must still resolve (reads are allowed)")
	assert.True(t, AsCoreCaller(got).Suspended, "resolved caller must be flagged suspended")
}

// TestOptionalCookieMiddleware_UnusableCookieIsAnonymous is this package's own
// contract rather than login's: whatever the decode answers, the middleware
// resolves nobody and still calls next. It is what keeps public browsing and
// public clones working when anything about the session goes wrong.
//
// The name cases are the second thing this package used not to do. A cookie
// naming "../../etc/passwd" or an empty string decoded to that string and went
// straight to the user lookup; now it never reaches the backend at all, and the
// assertion below is that it did not.
func TestOptionalCookieMiddleware_UnusableCookieIsAnonymous(t *testing.T) {
	cases := []struct {
		name   string
		cookie func(t *testing.T) *http.Cookie
	}{
		{"no cookie at all", func(*testing.T) *http.Cookie { return nil }},
		{"undecryptable value", func(*testing.T) *http.Cookie {
			return &http.Cookie{Name: login.CookieName, Value: "not-a-valid-fernet-token"}
		}},
		{"empty name in payload", func(t *testing.T) *http.Cookie {
			return &http.Cookie{Name: login.CookieName, Value: forgeCookie(t, "")}
		}},
		{"path separator in name", func(t *testing.T) *http.Cookie {
			return &http.Cookie{Name: login.CookieName, Value: forgeCookie(t, "../../etc/passwd")}
		}},
		{"newline in name", func(t *testing.T) *http.Cookie {
			return &http.Cookie{Name: login.CookieName, Value: forgeCookie(t, "alice\nWARN forged")}
		}},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			stub := &stubBackend{users: map[string]auth.AuthContext{
				"bigbes": sampleUser(1, "bigbes", auth.USER_TYPE_USER),
			}}
			withStubBackend(t, stub)

			got, code := runCookieMiddleware(t, c.cookie(t))
			assert.Nil(t, got, "must degrade to anonymous")
			assert.Equal(t, http.StatusOK, code, "must not reject")
			assert.Empty(t, stub.lookedUp, "an unusable cookie must not reach the user lookup")
		})
	}
}

func TestOptionalCookieMiddleware_UnknownUser(t *testing.T) {
	withStubBackend(t, &stubBackend{users: map[string]auth.AuthContext{}})
	got, code := runCookieMiddleware(t, &http.Cookie{
		Name:  login.CookieName,
		Value: forgeCookie(t, "ghost"),
	})
	assert.Nil(t, got, "unknown user must degrade to anonymous")
	assert.Equal(t, http.StatusOK, code, "must not reject")
}

func TestOptionalCookieMiddleware_BackendDownIsAnonymous(t *testing.T) {
	withStubBackend(t, &stubBackend{lookupErr: errBackendDown})
	got, code := runCookieMiddleware(t, &http.Cookie{
		Name:  login.CookieName,
		Value: forgeCookie(t, "bigbes"),
	})
	assert.Nil(t, got, "backend failure must degrade to anonymous")
	assert.Equal(t, http.StatusOK, code, "must not 500")
}