/
t3
/
s3-server
Обзор
Документация
Войти
/
t3
/
s3-server
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
master
internal/api/auth.go
217 строк
8 KB
Ivan Shibkikh
auth phase 4
25 июл 2026, 23:19
25 июл 2026, 23:19
02d1c69
Код
Авторство
О чём код?
package api import ( "errors" "net/http" "strings" golangjwt "github.com/golang-jwt/jwt/v5" s3jwt "gitverse.ru/t3/s3-server/internal/auth/jwt" "gitverse.ru/t3/s3-server/internal/auth/sigv4" "gitverse.ru/t3/s3-server/internal/config" ) // Context key for downstream handlers. type contextKey string const ( // ContextKeyAccessKey stores the authenticated access key (from Service Token or presigned URL). ContextKeyAccessKey contextKey = "authenticated_access_key" // ContextKeyUserID stores the subject (user ID) from the Service Token. ContextKeyUserID contextKey = "authenticated_user_id" // ContextKeyServiceClaims stores the verified Service Token claims (for audit). ContextKeyServiceClaims contextKey = "service_claims" ) // serviceTokenOnlyMiddleware verifies a Service Token and stores its claims in // the request context, but does NOT perform action-based authorization. The // downstream handler is responsible for checking the relevant permission(s). // // Used by endpoints (e.g. POST /presign) whose required permission depends on // the request body rather than the HTTP method/path. func serviceTokenOnlyMiddleware(verifier *s3jwt.Verifier) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if !strings.HasPrefix(authHeader, "Bearer ") { writeXMLError(w, r, http.StatusForbidden, "AccessDenied", "Missing authentication") return } if verifier == nil { writeXMLError(w, r, http.StatusInternalServerError, "InternalError", "JWT verifier not configured") return } tokenStr := strings.TrimPrefix(authHeader, "Bearer ") claims, err := verifier.VerifyServiceToken(r.Context(), tokenStr, "s3") if err != nil { if errors.Is(err, golangjwt.ErrTokenInvalidAudience) { writeXMLError(w, r, http.StatusForbidden, "AccessDenied", "Service token audience does not include s3") return } writeXMLError(w, r, http.StatusUnauthorized, "AccessDenied", "Invalid service token") return } r.Header.Set("X-Authenticated-Access-Key", claims.AccessKey) r.Header.Set("X-Authenticated-User-ID", claims.Subject) ctx := WithServiceClaims(r.Context(), claims) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // authMiddleware authenticates incoming requests via one of two paths: // // 1. Control Plane (Gateway → S3): an RS256 Service Token is presented in the // `Authorization: Bearer <token>` header. It is verified locally against the // STS-published JWKS (aud=s3, azp=gateway). Authorization is then decided // locally from the token's `permissions` claim — NO synchronous IAM call. // // 2. Data Plane (client ↔ S3): a presigned URL signed with the S3 service // credentials (SigV4). The signature is verified against the configured // service credentials and the URL must not have expired. // // The legacy static `X-Service-Token` and synchronous IAM `/auth/check` call // have been removed (hard switch, Phase 4). func authMiddleware(verifier *s3jwt.Verifier, svc config.ServiceCredentials) func(http.Handler) http.Handler { accessKeys := map[string]string{svc.AccessKey: svc.SecretKey} return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 1. Service Token (Control Plane). if authHeader := r.Header.Get("Authorization"); strings.HasPrefix(authHeader, "Bearer ") { handleServiceToken(w, r, verifier, authHeader, next) return } // 2. SigV4 presigned URL (Data Plane). if hasSigV4Params(r) { handlePresigned(w, r, accessKeys, next) return } writeXMLError(w, r, http.StatusForbidden, "AccessDenied", "Missing authentication") }) } } // handleServiceToken verifies a Service Token and performs local authorization. func handleServiceToken(w http.ResponseWriter, r *http.Request, verifier *s3jwt.Verifier, authHeader string, next http.Handler) { if verifier == nil { writeXMLError(w, r, http.StatusInternalServerError, "InternalError", "JWT verifier not configured") return } tokenStr := strings.TrimPrefix(authHeader, "Bearer ") claims, err := verifier.VerifyServiceToken(r.Context(), tokenStr, "s3") if err != nil { // A token whose audience targets a different service (e.g. "iam") is // forbidden, not merely unauthenticated. if errors.Is(err, golangjwt.ErrTokenInvalidAudience) { writeXMLError(w, r, http.StatusForbidden, "AccessDenied", "Service token audience does not include s3") return } writeXMLError(w, r, http.StatusUnauthorized, "AccessDenied", "Invalid service token") return } // Local authorization: check the required permission is present in the token. action := actionFromRequest(r) if !hasPermission(claims.Permissions, action) { writeXMLError(w, r, http.StatusForbidden, "AccessDenied", "Missing required permission: "+action) return } // Populate context with authenticated principal info. r.Header.Set("X-Authenticated-Access-Key", claims.AccessKey) r.Header.Set("X-Authenticated-User-ID", claims.Subject) ctx := WithServiceClaims(r.Context(), claims) next.ServeHTTP(w, r.WithContext(ctx)) } // handlePresigned verifies a SigV4 presigned URL against the S3 service credentials. func handlePresigned(w http.ResponseWriter, r *http.Request, accessKeys map[string]string, next http.Handler) { if sigv4.IsPresigned(r) { if err := sigv4.CheckExpiration(r); err != nil { writeXMLError(w, r, http.StatusForbidden, "AccessDenied", "Presigned URL has expired") return } } accessKey, err := sigv4.VerifySignature(r, accessKeys) if err != nil { writeXMLError(w, r, http.StatusForbidden, "AccessDenied", "Signature mismatch") return } r.Header.Set("X-Authenticated-Access-Key", accessKey) next.ServeHTTP(w, r) } // hasSigV4Params checks if the request contains SigV4 signature parameters // either via Authorization header or presigned URL query params. func hasSigV4Params(r *http.Request) bool { if strings.HasPrefix(r.Header.Get("Authorization"), "AWS4-HMAC-SHA256 ") { return true } if r.URL.Query().Get("X-Amz-Algorithm") == "AWS4-HMAC-SHA256" { return true } return false } // hasPermission reports whether the required permission (or the wildcard // "s3:*") is present in the token's permissions list. func hasPermission(perms []string, required string) bool { for _, p := range perms { if p == required || p == "s3:*" || p == "*" { return true } } return false } // actionFromRequest maps the HTTP request to an S3 permission string. func actionFromRequest(r *http.Request) string { method := r.Method hasKey := r.PathValue("key") != "" || strings.Contains(r.URL.Path, "/") && len(r.URL.Path) > 1 switch { case method == "GET" && r.URL.Path == "/": return "s3:ListAllMyBuckets" case method == "PUT" && !hasKey: return "s3:CreateBucket" case method == "DELETE" && !hasKey: return "s3:DeleteBucket" case method == "GET" && hasKey: return "s3:GetObject" case method == "HEAD" && hasKey: return "s3:GetObject" case method == "PUT" && hasKey: return "s3:PutObject" case method == "DELETE" && hasKey: return "s3:DeleteObject" case method == "POST": // InitiateMultipartUpload, CompleteMultipartUpload, etc. return "s3:PutObject" default: return "s3:*" } } // resourceFromRequest builds the S3 resource ARN from the request (kept for // audit/future use; local authorization is currently permission-based). func resourceFromRequest(r *http.Request) string { bucket := r.PathValue("bucket") key := r.PathValue("key") if bucket == "" { return "*" } if key != "" { return "arn:aws:s3:::" + bucket + "/" + key } return "arn:aws:s3:::" + bucket }