hub v0.152.0: operator access tokens with scopes (09 §3 decision 251, operator 2026-10-15 11:38)
gates / gates (push) Successful in 6m44s

- Configuration → Access tokens: named, scoped (group read/write), expiring (≤365 d), shown once,
  stored SHA-256 only, every use logged by name. Bearer fhp_… on the web surface only; /api/v1 refuses it.
- Route table = the code's single source (web/routes.go), pinned to ServeHTTP's switch; session-only:
  secrets shown/minted, deletes/resets, password, tokens.
- R-952 closed: /api/v1 route table names each caller, pinned to the switch; no-credential refusal test.
- Fail-open fixed: an empty operator password served the UI open (main skipped RequireAuth). Live hub never open.
- 05 §17, 04 §6.2, 13, 09 §3 251; register 155→157 (+R-1054, R-1055, R-1056, −R-952); STATUS.
- website: prowlarr-logo.svg (byte copy from felhom.eu-media) — the site gate refused every push.
Not deployed — waits for the operator's yes.

Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-15 12:20:35 +02:00
parent e92501f5a1
commit d62cc08b2e
37 changed files with 2567 additions and 21 deletions
+9
View File
@@ -245,6 +245,15 @@ func (h *Handler) requireGlobalKey(w http.ResponseWriter, r *http.Request) bool
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/v1")
// An operator access token (`05` §17, prefix fhp_) never reaches the box API: not a box route, not a global-key
// route. Refused here, before any handler, so no route can accept one by accident. Pinned by
// TestAPI_AccessTokenIsRefusedEverywhere.
if auth := r.Header.Get("Authorization"); strings.HasPrefix(strings.TrimSpace(strings.TrimPrefix(auth, "Bearer ")), store.APITokenPrefix) {
h.logger.Printf("[WARN] access token refused on the box API: %s %s", r.Method, r.URL.Path)
http.Error(w, "Forbidden: access tokens are not accepted on /api/v1", http.StatusForbidden)
return
}
switch {
case r.Method == http.MethodPost && path == "/report":
h.handleReport(w, r)