~bigbes/sr-ht-dolt

ref: e4bc92294ae4926d7aa93d5235db19fcb6d88790 sr-ht-dolt/authn/authn_test.go -rw-r--r-- 5.1 KiB
e4bc9229 — Eugene Blikh mcpsrv: add the beads tools 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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package authn

import (
	"context"
	"crypto/ed25519"
	"encoding/json"
	"os"
	"testing"
	"time"

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

	"sourcecraft.dev/bigbes/sr-ht-ecore/ecoretest"

	"github.com/dolthub/dolt/go/libraries/doltcore/creds"
	"github.com/vaughan0/go-ini"
	jose "gopkg.in/go-jose/go-jose.v2"
	"gopkg.in/go-jose/go-jose.v2/jwt"
)

// TestMain seeds the process-global crypto state from sr-ht-ecore's fixed test
// keyset, so that cookie encryption (crypto.Encrypt /
// DecryptWithoutExpiration) and bearer-token HMAC (auth.BearerToken.Encode /
// auth.DecodeBearerToken) share a keyset across the whole package's tests. No
// network, no Postgres.
//
// The keys are constants rather than generated ones: they secure nothing inside
// a test process, and a fixed keyset is what lets two packages of this service
// both initialise without the second rotating what the first sealed with.
func TestMain(m *testing.M) {
	ecoretest.InitCrypto()
	os.Exit(m.Run())
}

// testCtx returns a context carrying the config/service so that
// auth.DecodeGrants (which reads config.ServiceName) works in tests.
func testCtx() context.Context {
	return config.Context(context.Background(), ini.File{}, "dolt.sr.ht")
}

// stubBackend is an in-memory MetaBackend: LookupUser fills from a fixed table,
// IsRevoked consults a set of revoked hashes. Both can be told to fail
// (transient error) to exercise the temporary-error path.
type stubBackend struct {
	users     map[string]auth.AuthContext // keyed by lowercased username (no "~")
	revoked   map[[64]byte]bool
	lookupErr error
	revokeErr error

	// lookedUp records every name LookupUser was asked for, in order. It is how
	// a test asserts that a name was refused *before* it reached the backend —
	// which is what a validator buys, and which an unresolved caller alone
	// cannot distinguish from a name the backend merely did not know.
	lookedUp []string
}

func (s *stubBackend) LookupUser(ctx context.Context, username string, out *auth.AuthContext) error {
	s.lookedUp = append(s.lookedUp, username)
	if s.lookupErr != nil {
		return s.lookupErr
	}
	u, ok := s.users[normalize(username)]
	if !ok {
		return errUnknownUser
	}
	*out = u
	return nil
}

func (s *stubBackend) IsRevoked(ctx context.Context, username string, hash [64]byte, clientID string) (bool, error) {
	if s.revokeErr != nil {
		return false, s.revokeErr
	}
	return s.revoked[hash], nil
}

func normalize(username string) string {
	if len(username) > 0 && username[0] == '~' {
		username = username[1:]
	}
	return toLower(username)
}

func toLower(s string) string {
	b := []byte(s)
	for i, c := range b {
		if c >= 'A' && c <= 'Z' {
			b[i] = c + ('a' - 'A')
		}
	}
	return string(b)
}

var errUnknownUser = errTest("unknown user")
var errBackendDown = errTest("backend down")

type errTest string

func (e errTest) Error() string { return string(e) }

// withStubBackend installs a stub MetaBackend for the duration of a test and
// resets the token cache and clock, restoring everything afterwards.
func withStubBackend(t *testing.T, s MetaBackend) {
	t.Helper()
	prev := meta
	meta = s
	resetTokenCache()
	prevNow := nowFn
	t.Cleanup(func() {
		meta = prev
		nowFn = prevNow
		resetTokenCache()
	})
}

func resetTokenCache() {
	tokenCacheMu.Lock()
	tokenCache = map[[64]byte]cacheEntry{}
	tokenCacheMu.Unlock()
}

func sampleUser(id int, username, userType string) auth.AuthContext {
	notice := "suspended for testing"
	ac := auth.AuthContext{
		UserID:   id,
		Username: username,
		Email:    username + "@example.com",
		UserType: userType,
	}
	if userType == auth.USER_TYPE_SUSPENDED {
		ac.SuspensionNotice = &notice
	}
	return ac
}

// forgeCookie fernet-encrypts a unified-login cookie payload for username.
func forgeCookie(t *testing.T, username string) string {
	t.Helper()
	payload, err := json.Marshal(auth.AuthCookie{Name: username})
	if err != nil {
		t.Fatal(err)
	}
	return string(crypto.Encrypt(payload))
}

// forgePAT builds a valid meta personal access token for username with the
// given grants and expiry, HMAC-signed with the test keyset.
func forgePAT(username, grants string, expires time.Time) string {
	bt := auth.BearerToken{
		Version:  auth.TokenVersion,
		Expires:  auth.ToTimestamp(expires),
		Grants:   grants,
		Username: username,
	}
	return bt.Encode()
}

// mintDoltJWT builds an EdDSA JWS exactly as dolt's creds.RPCCreds does: kid +
// dolt_token_version headers, and aud/iss/sub/exp claims.
func mintDoltJWT(t *testing.T, priv ed25519.PrivateKey, kid, aud, iss, sub string, expiry time.Time) string {
	t.Helper()
	signingKey := jose.SigningKey{Algorithm: jose.EdDSA, Key: priv}
	opts := &jose.SignerOptions{ExtraHeaders: map[jose.HeaderKey]interface{}{
		jose.HeaderKey(creds.JWTKIDHeader):           kid,
		jose.HeaderKey(creds.DoltTokenVersionHeader): "2023.01",
	}}
	signer, err := jose.NewSigner(signingKey, opts)
	if err != nil {
		t.Fatal(err)
	}
	raw, err := jwt.Signed(signer).Claims(jwt.Claims{
		Audience: jwt.Audience{aud},
		Issuer:   iss,
		Subject:  sub,
		Expiry:   jwt.NewNumericDate(expiry),
	}).CompactSerialize()
	if err != nil {
		t.Fatal(err)
	}
	return raw
}