tenseleyflow/shithub / 35016e5

Browse files

Add reserved-name list covering shithub routes + GitHub-known reservations

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
35016e593d682b10a9156f65aa8eb999ad9dbc68
Parents
fa989f0
Tree
2628139

2 changed files

StatusFile+-
A internal/auth/reserved.go 102 0
A internal/auth/reserved_test.go 28 0
internal/auth/reserved.goadded
@@ -0,0 +1,102 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+// Package auth contains shared auth primitives used across the auth
4
+// surface (handlers, middleware, services). Reserved-name validation
5
+// lives here because it's read by signup *and* by username-change in S10.
6
+package auth
7
+
8
+import "strings"
9
+
10
+// reservedNames is the set of usernames that may NOT be claimed by users.
11
+// It covers:
12
+//   - Every static top-level route shithub registers (so /login can never
13
+//     resolve to a user named "login").
14
+//   - GitHub-known reservations we choose to mirror for parity.
15
+//   - A buffer of likely-future routes we'd rather lock in now than fight
16
+//     to reclaim from a squatter later.
17
+//
18
+// MAINTENANCE: when adding a new top-level route in any sprint, add the
19
+// leading path segment here. The audit test in
20
+// internal/web/handlers/handlers_test.go enforces this by walking the
21
+// registered chi router and checking every static segment.
22
+var reservedNames = map[string]struct{}{
23
+	// shithub-registered routes (S02–S05+).
24
+	"static":        {},
25
+	"healthz":       {},
26
+	"readyz":        {},
27
+	"metrics":       {},
28
+	"signup":        {},
29
+	"login":         {},
30
+	"logout":        {},
31
+	"password":      {},
32
+	"verify-email":  {},
33
+	"internal":      {},
34
+	"settings":      {},
35
+	"api":           {},
36
+	"admin":         {},
37
+	"new":           {},
38
+	"explore":       {},
39
+	"trending":      {},
40
+	"marketplace":   {},
41
+	"issues":        {},
42
+	"pulls":         {},
43
+	"notifications": {},
44
+	"watching":      {},
45
+	"stars":         {},
46
+	"orgs":          {},
47
+	"organizations": {},
48
+	"search":        {},
49
+	"about":         {},
50
+	"contact":       {},
51
+	"pricing":       {},
52
+	"site":          {},
53
+	"help":          {},
54
+	"docs":          {},
55
+	"blog":          {},
56
+	"security":      {},
57
+	"status":        {},
58
+	"terms":         {},
59
+	"privacy":       {},
60
+	"cookies":       {},
61
+	"oauth":         {},
62
+	"sessions":      {},
63
+	"saml":          {},
64
+	"sso":           {},
65
+	"webauthn":      {},
66
+	"two-factor":    {},
67
+	"recovery":      {},
68
+	"avatar":        {},
69
+	"avatars":       {},
70
+	"raw":           {},
71
+	"media":         {},
72
+	"assets":        {},
73
+	"favicon.ico":   {},
74
+	"robots.txt":    {},
75
+	"sitemap.xml":   {},
76
+	"git":           {},
77
+	"ssh":           {},
78
+	"public":        {},
79
+	"private":       {},
80
+	"shithub":       {},
81
+	"shithubd":      {},
82
+	"shithubbot":    {},
83
+}
84
+
85
+// IsReserved reports whether name is on the reserved list. The check is
86
+// case-insensitive — usernames are stored as citext, so we normalize here
87
+// to match.
88
+func IsReserved(name string) bool {
89
+	_, ok := reservedNames[strings.ToLower(name)]
90
+	return ok
91
+}
92
+
93
+// ReservedNames returns a copy of the reserved set as a slice. Used by
94
+// the route-audit test in handlers_test.go to confirm every registered
95
+// top-level path segment is present here.
96
+func ReservedNames() []string {
97
+	out := make([]string, 0, len(reservedNames))
98
+	for n := range reservedNames {
99
+		out = append(out, n)
100
+	}
101
+	return out
102
+}
internal/auth/reserved_test.goadded
@@ -0,0 +1,28 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package auth
4
+
5
+import "testing"
6
+
7
+func TestIsReserved(t *testing.T) {
8
+	t.Parallel()
9
+	yes := []string{"login", "logout", "settings", "api", "admin", "shithub", "explore"}
10
+	for _, n := range yes {
11
+		if !IsReserved(n) {
12
+			t.Errorf("expected %q to be reserved", n)
13
+		}
14
+	}
15
+	if !IsReserved("LOGIN") {
16
+		t.Error("reserved check should be case-insensitive")
17
+	}
18
+}
19
+
20
+func TestIsReserved_AllowsRegularUsernames(t *testing.T) {
21
+	t.Parallel()
22
+	no := []string{"alice", "bob-dev", "myproject1", "carol"}
23
+	for _, n := range no {
24
+		if IsReserved(n) {
25
+			t.Errorf("did not expect %q to be reserved", n)
26
+		}
27
+	}
28
+}