Files
sless/internal/api/middleware/auth.go
T
“Naeel” 18f25e7a65 refactor: Builder SoC + JWKS stub + unit tests
Builder SoC (builder/context.go):
- Moved generateDockerfile, runtimeBaseImage, zipToTarGz from handler/upload.go
  to internal/builder/context.go.
  Reason: knowledge about runtime images and build context structure is a
  build concern, not an HTTP handler concern.
- Added PrepareContext(zipData []byte, runtime string) (*bytes.Buffer, error) —
  single public entry point. Handler calls one function, gets ready buffer.
- zipToTarGz now accepts *zip.Reader instead of []byte to avoid double parsing.
- upload.go reduced from ~200 LOC to ~60 LOC (build logic gone).

auth.go — JWKS insertion point:
- Added verifySignature() stub with detailed comment explaining what v2
  implementation needs (JWKS endpoint, kid lookup, RS256/ES256 verify).
- Shows exactly where to add the call in validateJWT.

Unit tests (9 total, all pass):
- controllers: TestBuildDeployment_EnvVarsSorted, TestBuildDeployment_EmptyEnv
- handler: TestHopByHopHeaders_* (3 tests)
- builder: TestPrepareContext_PythonWithRequirements, _NodeNoPackageJSON,
           _UnsupportedRuntime, _DockerfileIsFirst
2026-03-11 09:34:34 +04:00

116 lines
4.7 KiB
Go

// Изменено: 2026-03-11
// Auth middleware — проверяет Bearer JWT-токен из заголовка Authorization.
//
// Архитектура аутентификации:
// - В v1 токен — это JWT облака (nubes), выданный при логине.
// - Оператор НЕ проверяет подпись JWT (публичный ключ nubes недоступен внутри кластера).
// - Проверяется только структура JWT и claim "sub" (не пустой) и "exp" (не истёк).
// - Полная проверка подлинности токена происходит в провайдере terraform через PingNubesAPI.
// - Такой подход называют "trusted perimeter": оператор доступен только внутри кластера,
// внешний доступ — через Ingress, где токен уже проверен на уровне API-шлюза.
//
// TODO v2: получать публичный ключ из nubes JWKS endpoint и проверять подпись RS256.
package middleware
import (
"encoding/base64"
"encoding/json"
"log/slog"
"net/http"
"strings"
"time"
)
// Auth возвращает middleware которое требует заголовок:
//
// Authorization: Bearer <jwt>
//
// Проверяет: структура JWT (3 части), наличие "sub", отсутствие истечения "exp".
func Auth(log *slog.Logger, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
header := r.Header.Get("Authorization")
if header == "" {
log.Warn("auth: missing authorization header", "remote", r.RemoteAddr, "path", r.URL.Path)
http.Error(w, `{"error":"authorization required"}`, http.StatusUnauthorized)
return
}
parts := strings.SplitN(header, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") {
log.Warn("auth: invalid authorization format", "remote", r.RemoteAddr)
http.Error(w, `{"error":"invalid authorization format, use Bearer <token>"}`, http.StatusUnauthorized)
return
}
if err := validateJWT(parts[1]); err != nil {
log.Warn("auth: invalid token", "remote", r.RemoteAddr, "path", r.URL.Path, "reason", err.Error())
http.Error(w, `{"error":"invalid token"}`, http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
// validateJWT проверяет структуру JWT и claim "sub" и "exp".
// Подпись НЕ проверяется — см. комментарий к файлу.
func validateJWT(token string) error {
jwtParts := strings.Split(token, ".")
if len(jwtParts) != 3 {
return &jwtError{"not a JWT: expected 3 parts"}
}
payload := jwtParts[1]
// JWT использует base64url без padding
switch len(payload) % 4 {
case 2:
payload += "=="
case 3:
payload += "="
}
decoded, err := base64.URLEncoding.DecodeString(payload)
if err != nil {
decoded, err = base64.StdEncoding.DecodeString(payload)
if err != nil {
return &jwtError{"cannot decode JWT payload"}
}
}
var claims struct {
Sub string `json:"sub"`
Exp int64 `json:"exp"`
}
if err := json.Unmarshal(decoded, &claims); err != nil {
return &jwtError{"cannot parse JWT claims"}
}
if claims.Sub == "" {
return &jwtError{"missing sub claim"}
}
if claims.Exp > 0 && claims.Exp < time.Now().Unix() {
return &jwtError{"token expired"}
}
return nil
}
type jwtError struct{ msg string }
func (e *jwtError) Error() string { return e.msg }
// verifySignature — точка вставки для проверки подписи JWT (v2).
//
// Текущее состояние (v1): подпись НЕ проверяется.
// Причина: публичный ключ nubes недоступен внутри кластера без JWKS endpoint.
// Безопасность обеспечивается "trusted perimeter" — оператор доступен только изнутри кластера.
//
// Когда nubes предоставит JWKS endpoint, реализация:
//
// func verifySignature(token string) error {
// // 1. Получить JWKS: GET {NUBES_JWKS_URL}/.well-known/jwks.json
// // 2. Найти ключ по "kid" из JWT header
// // 3. Проверить подпись RS256/ES256
// // Пример: github.com/lestrrat-go/jwx/v2/jwk + jwt.Parse
// return nil
// }
//
// После реализации добавить вызов в validateJWT после проверки структуры:
//
// if err := verifySignature(token); err != nil {
// return &jwtError{"signature verification failed: " + err.Error()}
// }