~bigbes/sr-ht-ecore

ref: 34cf83d43d19071055e7e7593d0646519771e625 sr-ht-ecore/bearer/status.go -rw-r--r-- 4.2 KiB
34cf83d4 — Eugene Blikh follow compare.sr.ht's rename to diff.sr.ht 9 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
package bearer

import (
	"errors"
	"net/http"
	"strconv"
)

// StatusFor is the instance's answer to "a token was refused — what does the
// caller see?", as one table rather than one per service.
//
// The mapping itself is not subtle. What makes it worth sharing is the arm
// that is easy to get wrong and impossible to notice: ErrUnavailable is 503,
// never 401. Reading an unreachable token daemon as "revoked" tells every CI
// job on the instance that its credential is bad for as long as tokens.sr.ht
// takes to restart, and somebody spends the evening re-minting tokens that
// were never broken. bench alone had this switch written out three times — in
// its REST surface, its MCP surface and its resolver — which is three chances
// to fold the unreachable case into the invalid one.
//
// The three refusals that ARE the caller's fault share one status and one
// sentence on purpose: telling a prober "that token exists but is revoked" is
// information they have not earned. Which of them it was belongs in the log.
//
// ErrNotOurs is the one arm a service must decide before asking: it means a
// well-formed token from another issuer, almost certainly a meta.sr.ht PAT,
// and SPEC ch. 6 step 2 leaves each service to accept it (dolt) or refuse it
// (bench, cover). Handle it first; reaching here it is a refusal, so it maps
// to 401.
//
// A nil error maps to 200 so a caller can write the status unconditionally.
func StatusFor(err error) int {
	switch {
	case err == nil:
		return http.StatusOK
	case errors.Is(err, ErrForbidden):
		return http.StatusForbidden
	case errors.Is(err, ErrUnavailable):
		return http.StatusServiceUnavailable
	default:
		// ErrInvalid, ErrRevoked, ErrNotOurs, and anything a future validator
		// step adds: an unrecognised failure is the caller's credential, not
		// the instance's health. A new sentinel that deserves 503 has to say
		// so here, which is the point of the default going this way — a
		// forgotten arm refuses a request rather than declaring the service
		// unwell.
		return http.StatusUnauthorized
	}
}

// IsRefusal reports whether err is one this package decided — that is, whether
// StatusFor's answer means anything for it.
//
// A service's own resolver returns more than bearer's vocabulary: the Postgres
// lookup it had to make, a context that expired, a bug. Handing those to
// StatusFor would answer 401 through its default arm, which is right for a
// credential and wrong for a database that did not answer — a caller told its
// token is bad re-mints a token that was never the problem. So a service that
// wraps this table guards the delegation:
//
//	if bearer.IsRefusal(err) {
//	    http.Error(w, msg, bearer.StatusFor(err))
//	    return
//	}
//	// anything else is ours, not the caller's
//
// Without this, each service spells out the sentinel list again, which is the
// five-line copy this package exists to stop.
//
// It answers for THIS package's vocabulary and nothing else, which is the trap
// in the guard above: a service whose own refusals do not wrap these sentinels
// sends its "bad token" straight into the else branch and answers 503 to a
// caller whose credential really was the problem. Either wrap — an
// ErrInvalidToken of your own that unwraps to ErrInvalid — or ask your own
// predicate first and reach this one only for what it can have produced.
func IsRefusal(err error) bool {
	for _, sentinel := range []error{
		ErrInvalid, ErrNotOurs, ErrForbidden, ErrRevoked, ErrUnavailable,
	} {
		if errors.Is(err, sentinel) {
			return true
		}
	}
	return false
}

// Challenge is the WWW-Authenticate value a 401 carries: the scheme, and the
// service's own config section as the realm.
//
// RFC 9110 requires the header on a 401, and every service on the instance was
// assembling the same string from the same constant. The realm is the section
// name ("bench.sr.ht") because that is what identifies the service everywhere
// else on this instance — in the config, in the nav, in a grant.
//
// The realm is quoted per RFC 9110 §11.6.1; a quote or backslash in it would
// end the parameter early, so the value is escaped rather than trusted. In
// practice a section name contains neither.
func Challenge(realm string) string {
	return "Bearer realm=" + strconv.Quote(realm)
}