package httpapi import ( "context" "net/http" "strings" ) type ctxKey int const authCtxKey ctxKey = 1 // Auth holds resolved API identity for a request. type Auth struct { TenantID string Role string // viewer, editor, operator, node Token string APIKeyID string // non-empty for DB-managed keys } func authFromContext(ctx context.Context) (Auth, bool) { a, ok := ctx.Value(authCtxKey).(Auth) return a, ok } type apiKeyRecord struct { token string tenantID string role string keyID string // set for DB-managed keys (last_used_at) } func parseAPIKeysSpec(spec string) []apiKeyRecord { spec = strings.TrimSpace(spec) if spec == "" { return nil } var out []apiKeyRecord for _, part := range strings.Split(spec, ",") { part = strings.TrimSpace(part) if part == "" { continue } fields := strings.Split(part, "|") if len(fields) != 3 { continue } out = append(out, apiKeyRecord{ token: strings.TrimSpace(fields[0]), tenantID: strings.TrimSpace(fields[1]), role: strings.TrimSpace(fields[2]), }) } return out } func (s *Server) authMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h := r.Header.Get("Authorization") const p = "Bearer " if !strings.HasPrefix(h, p) { writeProblem(w, http.StatusUnauthorized, "Unauthorized", "missing or invalid bearer token") return } raw := strings.TrimSpace(strings.TrimPrefix(h, p)) if raw == "dev" { if a, ok := s.devAuth(); ok { r = r.WithContext(context.WithValue(r.Context(), authCtxKey, a)) next.ServeHTTP(w, r) return } } matched, ok := s.keyResolver.Lookup(raw) if !ok { writeProblem(w, http.StatusUnauthorized, "Unauthorized", "unknown api key") return } a := Auth{TenantID: matched.tenantID, Role: matched.role, Token: raw, APIKeyID: matched.keyID} if matched.keyID != "" { go func(id string) { _ = s.store.TouchAPIKeyLastUsed(id) }(matched.keyID) } r = r.WithContext(context.WithValue(r.Context(), authCtxKey, a)) next.ServeHTTP(w, r) }) } func (s *Server) devAuth() (Auth, bool) { tid, _, _, _, _ := s.store.DemoIDs() if tid == "" { return Auth{}, false } return Auth{TenantID: tid, Role: "operator", Token: "dev"}, true } func roleLevel(role string) int { switch strings.ToLower(role) { case "viewer": return 1 case "editor": return 2 case "operator": return 3 default: return 0 } } // requireAtLeast rejects node role and enforces viewer/editor/operator ladder. func (s *Server) requireAtLeast(w http.ResponseWriter, a Auth, need string) bool { if strings.ToLower(a.Role) == "node" { writeProblem(w, http.StatusForbidden, "Forbidden", "node role cannot access this resource") return false } if roleLevel(a.Role) < roleLevel(need) { writeProblem(w, http.StatusForbidden, "Forbidden", "insufficient role") return false } return true } func (s *Server) requireNode(w http.ResponseWriter, a Auth) bool { if strings.ToLower(a.Role) != "node" { writeProblem(w, http.StatusForbidden, "Forbidden", "node role required") return false } return true }