// Изменено: 2026-04-09 // Auth middleware для shared-sqs: извлекает AccessKeyId из AWS Authorization header // и помещает найденного тенанта в context запроса. package auth import ( "context" "encoding/xml" "net/http" "strings" "shared-sqs/app/tenant" ) // TenantContextKey — ключ для хранения тенанта в request context. // Тип contextKey предотвращает конфликты с другими пакетами. type contextKey string const TenantContextKey contextKey = "tenant" // AuthMiddleware — middleware: ищет тенанта по AccessKeyId из AWS Authorization header. // Пропускает /health и /admin/** без tenant-аутентификации. // Ловушка #4: не ставим короткий таймаут — ReceiveMessage с long polling держит соединение до 20 сек. func AuthMiddleware(store *tenant.TenantStore) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // /health — без auth if r.URL.Path == "/health" { next.ServeHTTP(w, r) return } // /admin/** — отдельная auth (bearer token, см. admin_handlers.go) if strings.HasPrefix(r.URL.Path, "/admin/") { next.ServeHTTP(w, r) return } accessKeyID := extractAccessKeyID(r) if accessKeyID == "" { writeSQSAuthError(w, "MissingAuthenticationToken", "Request must contain either AccessKeyId or X-Amz-Credential") return } t, ok := store.GetByAccessKey(accessKeyID) if !ok || !t.Active { writeSQSAuthError(w, "InvalidClientTokenId", "The security token included in the request is invalid") return } ctx := context.WithValue(r.Context(), TenantContextKey, t) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // extractAccessKeyID — извлекает AWS AccessKeyId из запроса. // Поддерживает оба варианта: Authorization header (Signature V4) и X-Amz-Credential query param (presigned URLs). // Ловушка #3: AWS CLI ВСЕГДА отправляет Signature V4 — нужно парсить, даже не проверяя подпись. // Ловушка #5: X-Amz-Security-Token (STS) — игнорируем. func extractAccessKeyID(r *http.Request) string { // Вариант 1: Authorization header // Формат: "AWS4-HMAC-SHA256 Credential={AccessKeyId}/{date}/{region}/sqs/aws4_request, ..." auth := r.Header.Get("Authorization") if strings.HasPrefix(auth, "AWS4-HMAC-SHA256") { idx := strings.Index(auth, "Credential=") if idx >= 0 { rest := auth[idx+len("Credential="):] slashIdx := strings.Index(rest, "/") if slashIdx > 0 { return rest[:slashIdx] } } } // Вариант 2: Query parameter (presigned URLs) // Формат: X-Amz-Credential={AccessKeyId}/{date}/{region}/sqs/aws4_request if cred := r.URL.Query().Get("X-Amz-Credential"); cred != "" { parts := strings.SplitN(cred, "/", 2) if len(parts) > 0 && parts[0] != "" { return parts[0] } } return "" } // sqsAuthError — AWS-совместимый XML ответ об ошибке аутентификации. type sqsAuthError struct { XMLName xml.Name `xml:"ErrorResponse"` Error sqsErrorBody `xml:"Error"` RequestID string `xml:"RequestId"` } type sqsErrorBody struct { Type string `xml:"Type"` Code string `xml:"Code"` Message string `xml:"Message"` } // writeSQSAuthError — отвечает AWS-совместимым XML с кодом 403. func writeSQSAuthError(w http.ResponseWriter, code, message string) { w.Header().Set("Content-Type", "application/xml") w.WriteHeader(http.StatusForbidden) resp := sqsAuthError{ Error: sqsErrorBody{ Type: "Sender", Code: code, Message: message, }, RequestID: "00000000-0000-0000-0000-000000000000", } data, _ := xml.Marshal(resp) w.Write(data) }