coverage~bigbes/sr-ht-ecore89fa694cbearer/status.go

Coverage
100.0% 10/10 statements
Δ
+0.0
Blob
8362452
Uncovered nothing — every instrumented line ran
1 package bearer
2
3 import (
4 "errors"
5 "net/http"
6 "strconv"
7 )
8
9 // StatusFor is the instance's answer to "a token was refused — what does the
10 // caller see?", as one table rather than one per service.
11 //
12 // The mapping itself is not subtle. What makes it worth sharing is the arm
13 // that is easy to get wrong and impossible to notice: ErrUnavailable is 503,
14 // never 401. Reading an unreachable token daemon as "revoked" tells every CI
15 // job on the instance that its credential is bad for as long as tokens.sr.ht
16 // takes to restart, and somebody spends the evening re-minting tokens that
17 // were never broken. bench alone had this switch written out three times — in
18 // its REST surface, its MCP surface and its resolver — which is three chances
19 // to fold the unreachable case into the invalid one.
20 //
21 // The three refusals that ARE the caller's fault share one status and one
22 // sentence on purpose: telling a prober "that token exists but is revoked" is
23 // information they have not earned. Which of them it was belongs in the log.
24 //
25 // ErrNotOurs is the one arm a service must decide before asking: it means a
26 // well-formed token from another issuer, almost certainly a meta.sr.ht PAT,
27 // and SPEC ch. 6 step 2 leaves each service to accept it (dolt) or refuse it
28 // (bench, cover). Handle it first; reaching here it is a refusal, so it maps
29 // to 401.
30 //
31 // A nil error maps to 200 so a caller can write the status unconditionally.
32 9 func StatusFor(err error) int {
33 9 switch {
34 1 case err == nil:
35 1 return http.StatusOK
36 2 case errors.Is(err, ErrForbidden):
37 2 return http.StatusForbidden
38 2 case errors.Is(err, ErrUnavailable):
39 2 return http.StatusServiceUnavailable
40 4 default:
41 4 // ErrInvalid, ErrRevoked, ErrNotOurs, and anything a future validator
42 4 // step adds: an unrecognised failure is the caller's credential, not
43 4 // the instance's health. A new sentinel that deserves 503 has to say
44 4 // so here, which is the point of the default going this way — a
45 4 // forgotten arm refuses a request rather than declaring the service
46 4 // unwell.
47 4 return http.StatusUnauthorized
48 }
49 }
50
51 // IsRefusal reports whether err is one this package decided — that is, whether
52 // StatusFor's answer means anything for it.
53 //
54 // A service's own resolver returns more than bearer's vocabulary: the Postgres
55 // lookup it had to make, a context that expired, a bug. Handing those to
56 // StatusFor would answer 401 through its default arm, which is right for a
57 // credential and wrong for a database that did not answer — a caller told its
58 // token is bad re-mints a token that was never the problem. So a service that
59 // wraps this table guards the delegation:
60 //
61 // if bearer.IsRefusal(err) {
62 // http.Error(w, msg, bearer.StatusFor(err))
63 // return
64 // }
65 // // anything else is ours, not the caller's
66 //
67 // Without this, each service spells out the sentinel list again, which is the
68 // five-line copy this package exists to stop.
69 //
70 // It answers for THIS package's vocabulary and nothing else, which is the trap
71 // in the guard above: a service whose own refusals do not wrap these sentinels
72 // sends its "bad token" straight into the else branch and answers 503 to a
73 // caller whose credential really was the problem. Either wrap — an
74 // ErrInvalidToken of your own that unwraps to ErrInvalid — or ask your own
75 // predicate first and reach this one only for what it can have produced.
76 13 func IsRefusal(err error) bool {
77 13 for _, sentinel := range []error{
78 13 ErrInvalid, ErrNotOurs, ErrForbidden, ErrRevoked, ErrUnavailable,
79 45 } {
80 45 if errors.Is(err, sentinel) {
81 10 return true
82 10 }
83 }
84 3 return false
85 }
86
87 // Challenge is the WWW-Authenticate value a 401 carries: the scheme, and the
88 // service's own config section as the realm.
89 //
90 // RFC 9110 requires the header on a 401, and every service on the instance was
91 // assembling the same string from the same constant. The realm is the section
92 // name ("bench.sr.ht") because that is what identifies the service everywhere
93 // else on this instance — in the config, in the nav, in a grant.
94 //
95 // The realm is quoted per RFC 9110 §11.6.1; a quote or backslash in it would
96 // end the parameter early, so the value is escaped rather than trusted. In
97 // practice a section name contains neither.
98 3 func Challenge(realm string) string {
99 3 return "Bearer realm=" + strconv.Quote(realm)
100 3 }