126 lines
4.2 KiB
Go
126 lines
4.2 KiB
Go
package api
|
||
|
||
import (
|
||
"context"
|
||
"crypto/sha256"
|
||
"encoding/base64"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"strings"
|
||
)
|
||
|
||
// ctxKeyNS — ключ для хранения namespace пользователя в context.Context.
|
||
// Использует приватный тип чтобы избежать коллизий с ключами из других пакетов.
|
||
type ctxKeyNS struct{}
|
||
|
||
// 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
|
||
|
||
if s.testMode {
|
||
sub := strings.TrimSpace(r.Header.Get("X-Test-Sub"))
|
||
if sub == "" {
|
||
sub = strings.TrimSpace(r.Header.Get("X-Auth-Token"))
|
||
}
|
||
if sub == "" {
|
||
writeJSONError(w, http.StatusUnauthorized, "test mode: X-Test-Sub required")
|
||
return
|
||
}
|
||
h32 := sha256.Sum256([]byte(sub))
|
||
ns = "fission-" + hex.EncodeToString(h32[:8])
|
||
} else {
|
||
token := strings.TrimSpace(r.Header.Get("X-Auth-Token"))
|
||
env := strings.TrimSpace(strings.ToLower(r.Header.Get("X-Auth-Env")))
|
||
if _, ok := deckAPIs[env]; !ok {
|
||
env = "test"
|
||
}
|
||
if token == "" {
|
||
writeJSONError(w, http.StatusUnauthorized, "unauthorized")
|
||
return
|
||
}
|
||
if err := s.validateDeckToken(token, env); err != nil {
|
||
writeJSONError(w, http.StatusUnauthorized, "unauthorized")
|
||
return
|
||
}
|
||
var err error
|
||
ns, err = namespaceFromJWT(token)
|
||
if err != nil {
|
||
ns = s.ns
|
||
}
|
||
}
|
||
|
||
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))
|
||
}
|
||
}
|
||
|
||
// 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
|
||
}
|