mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-31 01:10:41 +01:00
s3api: answer IAM/STS failures in the query-protocol envelope
Embedded IAM and STS routes now report authentication, form-parse and authorization failures with the IAM ErrorResponse body instead of the S3 Error envelope, so IAM SDK clients can parse them. Requests signed for s3 keep the S3 envelope, keyed off the credential scope. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
e068bd2167
commit
c05a296ba1
4 files changed
+74
-13
No files matched your search
@@ -126,6 +126,27 @@ func normalizePayloadHash(payloadHashValue string) string {
|
||||
return payloadHashValue
|
||||
}
|
||||
|
||||
// requestSigningService extracts the service name from the SigV4 credential
|
||||
// scope (…/region/service/aws4_request) so a failed request can be answered in
|
||||
// that service's error envelope.
|
||||
func requestSigningService(r *http.Request) string {
|
||||
credential := r.URL.Query().Get("X-Amz-Credential")
|
||||
if credential == "" {
|
||||
v4Auth := strings.Replace(r.Header.Get("Authorization"), " ", "", -1)
|
||||
v4Auth = strings.TrimPrefix(v4Auth, signV4Algorithm)
|
||||
for _, field := range strings.Split(v4Auth, ",") {
|
||||
if cred, ok := strings.CutPrefix(field, "Credential="); ok {
|
||||
credential = cred
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if parts := strings.Split(credential, "/"); len(parts) == 5 {
|
||||
return parts[3]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// signValues data type represents structured form of AWS Signature V4 header.
|
||||
type signValues struct {
|
||||
Credential credentialHeader
|
||||
|
||||
@@ -196,10 +196,32 @@ func newIamErrorResponse(errCode string, errMsg string, requestID string) iamErr
|
||||
return errorResp
|
||||
}
|
||||
|
||||
func (e *EmbeddedIamApi) writeIamErrorResponse(w http.ResponseWriter, r *http.Request, reqID string, iamErr *iamError) {
|
||||
// iamAuthErrorCode maps an S3-side signature failure onto the IAM
|
||||
// query-protocol code IAM clients expect for the same request.
|
||||
func iamAuthErrorCode(errCode s3err.ErrorCode) string {
|
||||
switch errCode {
|
||||
case s3err.ErrInvalidAccessKeyID:
|
||||
return "InvalidClientTokenId"
|
||||
case s3err.ErrSignatureDoesNotMatch:
|
||||
return "SignatureDoesNotMatch"
|
||||
case s3err.ErrAccessDenied:
|
||||
return "AccessDenied"
|
||||
case s3err.ErrExpiredToken:
|
||||
return "ExpiredToken"
|
||||
case s3err.ErrRequestTimeTooSkewed:
|
||||
return "RequestTimeTooSkewed"
|
||||
case s3err.ErrInternalError:
|
||||
return iam.ErrCodeServiceFailureException
|
||||
default:
|
||||
return "IncompleteSignature"
|
||||
}
|
||||
}
|
||||
|
||||
func writeIamErrorResponse(w http.ResponseWriter, r *http.Request, reqID string, iamErr *iamError) {
|
||||
if iamErr == nil {
|
||||
glog.Errorf("writeIamErrorResponse called with nil error")
|
||||
internalResp := newIamErrorResponse(iam.ErrCodeServiceFailureException, "Internal server error", reqID)
|
||||
internalResp.Error.Type = "Receiver"
|
||||
s3err.WriteXMLResponse(w, r, http.StatusInternalServerError, internalResp)
|
||||
return
|
||||
}
|
||||
@@ -2562,15 +2584,19 @@ func (iam *IdentityAccessManagement) AuthIamManagement(f http.HandlerFunc) http.
|
||||
// needs to hash the body for IAM requests (service != "s3").
|
||||
// The streamHashRequestBody function in auth_signature_v4.go preserves the body
|
||||
// after reading it, so ParseForm() will work correctly after authentication.
|
||||
r, reqID := request_id.Ensure(r)
|
||||
identity, errCode := iam.AuthSignatureOnly(r)
|
||||
if errCode != s3err.ErrNone {
|
||||
s3err.WriteErrorResponse(w, r, errCode)
|
||||
writeIamErrorResponse(w, r, reqID, &iamError{
|
||||
Code: iamAuthErrorCode(errCode),
|
||||
Error: errors.New(s3err.GetAPIError(errCode).Description),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Now parse form to get Action and UserName (body was preserved by auth)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
|
||||
writeIamErrorResponse(w, r, reqID, &iamError{Code: "InvalidInput", Error: err})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2578,7 +2604,10 @@ func (iam *IdentityAccessManagement) AuthIamManagement(f http.HandlerFunc) http.
|
||||
// from, so the authorized target and the acted-on target cannot differ.
|
||||
action := r.Form.Get("Action")
|
||||
if errCode := iam.AuthorizeIamAction(r, identity, action, iamTargetUserName(action, r)); errCode != s3err.ErrNone {
|
||||
s3err.WriteErrorResponse(w, r, errCode)
|
||||
writeIamErrorResponse(w, r, reqID, &iamError{
|
||||
Code: iamAuthErrorCode(errCode),
|
||||
Error: errors.New(s3err.GetAPIError(errCode).Description),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3003,7 +3032,7 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s
|
||||
func (e *EmbeddedIamApi) DoActions(w http.ResponseWriter, r *http.Request) {
|
||||
r, reqID := request_id.Ensure(r)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
|
||||
writeIamErrorResponse(w, r, reqID, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err})
|
||||
return
|
||||
}
|
||||
values := r.PostForm
|
||||
@@ -3019,7 +3048,7 @@ func (e *EmbeddedIamApi) DoActions(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
response, iamErr := e.ExecuteAction(r.Context(), values, false, reqID)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, reqID, iamErr)
|
||||
writeIamErrorResponse(w, r, reqID, iamErr)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -402,9 +402,8 @@ func TestUpdateThumbprintAndTags(t *testing.T) {
|
||||
// A refusal to change a config-file provider reaches the client as AWS sends
|
||||
// it (400 UnmodifiableEntity), not as an internal error clients retry.
|
||||
func TestUnmodifiableEntityIsAClientError(t *testing.T) {
|
||||
api := NewEmbeddedIamApiForTest()
|
||||
rec := httptest.NewRecorder()
|
||||
api.writeIamErrorResponse(rec, httptest.NewRequest(http.MethodPost, "/", nil), "req-1",
|
||||
writeIamErrorResponse(rec, httptest.NewRequest(http.MethodPost, "/", nil), "req-1",
|
||||
oidcMutationError(fmt.Errorf("%w: arn:aws:iam:::oidc-provider/static.example", integration.ErrOIDCProviderStatic)))
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want 400", rec.Code)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
@@ -730,10 +731,21 @@ func (s3a *S3ApiServer) handleCORSOriginValidation(w http.ResponseWriter, r *htt
|
||||
// UnifiedPostHandler handles authenticated POST requests to the root path
|
||||
// It inspects the Action parameter to dispatch to either STS or IAM handlers
|
||||
func (s3a *S3ApiServer) UnifiedPostHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Failures on this route answer in the IAM/STS query-protocol envelope
|
||||
// unless the request was actually signed for s3.
|
||||
r, reqID := request_id.Ensure(r)
|
||||
writeQueryError := func(iamCode string, s3Code s3err.ErrorCode) {
|
||||
if requestSigningService(r) == "s3" {
|
||||
s3err.WriteErrorResponse(w, r, s3Code)
|
||||
return
|
||||
}
|
||||
writeIamErrorResponse(w, r, reqID, &iamError{Code: iamCode, Error: errors.New(s3err.GetAPIError(s3Code).Description)})
|
||||
}
|
||||
|
||||
// 1. Authenticate (preserves body)
|
||||
identity, errCode := s3a.iam.AuthSignatureOnly(r)
|
||||
if errCode != s3err.ErrNone {
|
||||
s3err.WriteErrorResponse(w, r, errCode)
|
||||
writeQueryError(iamAuthErrorCode(errCode), errCode)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -745,7 +757,7 @@ func (s3a *S3ApiServer) UnifiedPostHandler(w http.ResponseWriter, r *http.Reques
|
||||
bodyBytes, err = readRequestBody(r, iamRequestBodyLimit)
|
||||
if err != nil {
|
||||
glog.Errorf("failed to read request body: %v", err)
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
|
||||
writeQueryError("InvalidInput", s3err.ErrInvalidRequest)
|
||||
return
|
||||
}
|
||||
r.Body.Close()
|
||||
@@ -754,7 +766,7 @@ func (s3a *S3ApiServer) UnifiedPostHandler(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
|
||||
writeQueryError("InvalidInput", s3err.ErrInvalidRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -781,7 +793,7 @@ func (s3a *S3ApiServer) UnifiedPostHandler(w http.ResponseWriter, r *http.Reques
|
||||
// IAM
|
||||
// IAM API requests must be authenticated - reject nil identity
|
||||
if identity == nil {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
||||
writeQueryError("AccessDenied", s3err.ErrAccessDenied)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -792,7 +804,7 @@ func (s3a *S3ApiServer) UnifiedPostHandler(w http.ResponseWriter, r *http.Reques
|
||||
// UserName comes from the body only, the same place DoActions reads it
|
||||
// from, so the authorized target and the acted-on target cannot differ.
|
||||
if s3a.iam.AuthorizeIamAction(r, identity, action, iamTargetUserName(action, r)) != s3err.ErrNone {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
||||
writeQueryError("AccessDenied", s3err.ErrAccessDenied)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user