package api import ( "context" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "net/http" "strings" ) // ctxKeyNS — ключ для хранения namespace пользователя в context.Context. // Использует приватный тип чтобы избежать коллизий с ключами из других пакетов. type ctxKeyNS struct{} func authTokenFromRequest(r *http.Request) string { token := strings.TrimSpace(r.Header.Get("X-Auth-Token")) if token != "" { return token } authz := strings.TrimSpace(r.Header.Get("Authorization")) if authz == "" { return "" } const bearerPrefix = "Bearer " if len(authz) >= len(bearerPrefix) && strings.EqualFold(authz[:len(bearerPrefix)], bearerPrefix) { return strings.TrimSpace(authz[len(bearerPrefix):]) } return "" } // userNS возвращает namespace пользователя из контекста запроса. // Устанавливается в authMiddleware после успешной аутентификации. func (s *Server) userNS(r *http.Request) string { if ns, ok := r.Context().Value(ctxKeyNS{}).(string); ok && ns != "" { return ns } // Fallback: использовать системный namespace (не должно происходить в prod) return s.ns } // authMiddleware оборачивает handler, добавляя аутентификацию и инициализацию namespace. // // В testMode (FISSION_TEST_MODE=true): // - Deck API не вызывается // - X-Test-Sub или X-Auth-Token задают sub → разные namespace-ы для тестирования // // В production: // - X-Auth-Token валидируется через Deck API // - Namespace вычисляется из JWT claim "sub" func (s *Server) authMiddleware(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var ns string env := strings.TrimSpace(strings.ToLower(r.Header.Get("X-Auth-Env"))) if _, ok := deckAPIs[env]; !ok { env = "test" } if s.testMode { sub := strings.TrimSpace(r.Header.Get("X-Test-Sub")) if sub != "" { ns = namespaceFromSub(sub) } else { token := authTokenFromRequest(r) resolvedNS, err := s.resolveNamespaceForToken(token, env, true) if err != nil { writeJSONError(w, http.StatusUnauthorized, "unauthorized") return } ns = resolvedNS } } else { token := authTokenFromRequest(r) resolvedNS, err := s.resolveNamespaceForToken(token, env, false) if err != nil { writeJSONError(w, http.StatusUnauthorized, "unauthorized") return } ns = resolvedNS } ctx := context.WithValue(r.Context(), ctxKeyNS{}, ns) // Гарантируем что namespace + RBAC + quota + netpol существуют. // EnsureUserNS реализует singleflight + кэш + семафор параллелизма. if ensureErr := s.nsManager.EnsureUserNS(ctx, ns); ensureErr != nil { writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("ensure namespace: %v", ensureErr)) return } h(w, r.WithContext(ctx)) } } func namespaceFromSub(sub string) string { h32 := sha256.Sum256([]byte(sub)) return "fission-" + hex.EncodeToString(h32[:8]) } func (s *Server) resolveNamespaceForToken(token, env string, allowTestSub bool) (string, error) { token = strings.TrimSpace(token) if token == "" { if allowTestSub { return "", fmt.Errorf("test mode: X-Test-Sub required") } return "", fmt.Errorf("unauthorized") } if err := s.validateDeckToken(token, env); err == nil { ns, nsErr := namespaceFromJWT(token) if nsErr != nil { return s.ns, nil } return ns, nil } if allowTestSub && strings.Contains(token, "@") { return namespaceFromSub(token), nil } return "", fmt.Errorf("invalid token") } // namespaceFromJWT декодирует JWT payload (без верификации подписи), // извлекает claim "sub" и вычисляет namespace: "fission-" + hex(SHA256(sub)[:8]). // // Подпись не проверяется — токен уже валидирован через Deck API (validateDeckToken). // Здесь нам нужен только deterministic namespace name из sub claim. func namespaceFromJWT(token string) (string, error) { parts := strings.SplitN(token, ".", 3) if len(parts) != 3 { return "", fmt.Errorf("invalid JWT format") } payload := parts[1] // JWT использует base64url без padding — добавляем если нужно switch len(payload) % 4 { case 2: payload += "==" case 3: payload += "=" } // base64url без стандартного padding — пробуем оба варианта decoded, err := base64.URLEncoding.DecodeString(payload) if err != nil { decoded, err = base64.StdEncoding.DecodeString(payload) if err != nil { return "", fmt.Errorf("decode JWT payload: %w", err) } } var claims map[string]any if err := json.Unmarshal(decoded, &claims); err != nil { return "", fmt.Errorf("unmarshal JWT claims: %w", err) } sub, _ := claims["sub"].(string) if sub == "" { return "", fmt.Errorf("JWT missing sub claim") } h := sha256.Sum256([]byte(sub)) return "fission-" + hex.EncodeToString(h[:8]), nil }