oficina / sheets Public

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

Plain git — no account needed to clone.

Download

Download this file

1package guards
2
3import (
4 "go/ast"
5 "go/parser"
6 "go/token"
7 "strconv"
8 "testing"
9)
10
11// sessions.UserID is unusable in this app, and unusable in a way that
12// does not announce itself.
13//
14// It parses the current session's Subject as an integer and returns
15// (0, false) when that fails. rastrillo/auth sets Subject to the
16// signed-in email address, so the parse fails on EVERY request — UserID
17// can never legitimately succeed here. rastrillo's own magic-links page
18// calls this "the most expensive mistake you can make with this plugin".
19//
20// That would be harmless if it errored. It returns 0, and the idiom the
21// signature invites is `uid, _ := sessions.UserID(r)`. Dropping the ok
22// scopes every subsequent query to user 0: no rows match, so the page
23// renders as a plausible empty list rather than failing. Same shape as
24// scope.Owned on a team-shared table (AGENTS.md) — both fail as a
25// believable answer instead of an error.
26//
27// Use sessions.Current(r) and its Subject (the address) instead.
28//
29// This walks the AST for CALL EXPRESSIONS rather than grepping for the
30// string. A textual match also hits comments, so the moment this rule is
31// explained anywhere in prose the guard needs an exemption — and an
32// exemption list is a second thing to maintain that quietly grows until
33// it covers the violation it was meant to catch. Docs hit exactly that
34// (four warning comments, no call sites) and made the same change.
35const (
36 sessionsPkg = "amadan.net/rastrillo/rastrillo/sessions"
37 forbiddenIdent = "UserID"
38)
39
40func TestNoSessionsUserID(t *testing.T) {
41 root := repoRoot(t)
42 for _, path := range goFiles(t, root) {
43 rel := relPath(t, root, path)
44 fset := token.NewFileSet()
45 f, err := parser.ParseFile(fset, path, nil, 0)
46 if err != nil {
47 t.Fatalf("guards: parse %s: %v", rel, err)
48 }
49
50 // Local name for the sessions package in THIS file: it may be
51 // aliased, dot-imported, or absent. No import, no violation
52 // possible.
53 // A dot-import puts UserID in the file scope as a bare
54 // identifier, so a SelectorExpr check alone would miss it
55 // entirely. Docs hit exactly that when they took this guard.
56 local, dotImported := "", false
57 for _, imp := range f.Imports {
58 p, err := strconv.Unquote(imp.Path.Value)
59 if err != nil || p != sessionsPkg {
60 continue
61 }
62 local = "sessions"
63 if imp.Name != nil {
64 local = imp.Name.Name
65 }
66 if local == "." {
67 dotImported = true
68 }
69 }
70 if local == "" {
71 continue
72 }
73
74 ast.Inspect(f, func(n ast.Node) bool {
75 call, ok := n.(*ast.CallExpr)
76 if !ok {
77 return true
78 }
79 switch fn := call.Fun.(type) {
80 case *ast.SelectorExpr:
81 pkg, ok := fn.X.(*ast.Ident)
82 if !ok || pkg.Name != local || fn.Sel.Name != forbiddenIdent {
83 return true
84 }
85 case *ast.Ident:
86 if !dotImported || fn.Name != forbiddenIdent {
87 return true
88 }
89 default:
90 return true
91 }
92 qualifier, use := local+".", local+".Current(r).Subject"
93 if dotImported {
94 qualifier, use = "", "Current(r).Subject"
95 }
96 pos := fset.Position(call.Pos())
97 t.Errorf("%s:%d: calls %s%s, which returns (0, false) for every "+
98 "request in this app because auth's Subject is an email address "+
99 "— a dropped ok scopes the query to user 0 and renders as an "+
100 "empty list. Use %s.",
101 rel, pos.Line, qualifier, forbiddenIdent, use)
102 return true
103 })
104 }
105}
106