- Standalone SQS-service repository - Multi-tenant message queue service, AWS SQS compatible - Based on GoAws, with mutable tenants, auth, WebUI, Redis persistence - Ready for independent development and deployment - See doc/ and README.md for architecture and usage
115 lines
3.8 KiB
Go
115 lines
3.8 KiB
Go
// Изменено: 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)
|
||
}
|