diff --git a/cmd/auth-handler.go b/cmd/auth-handler.go index 260699a84..1f71c1dc9 100644 --- a/cmd/auth-handler.go +++ b/cmd/auth-handler.go @@ -185,12 +185,12 @@ func getSessionToken(r *http.Request) (token string) { // Fetch claims in the security token returned by the client, doesn't return // errors - upon errors the returned claims map will be empty. func mustGetClaimsFromToken(r *http.Request) map[string]interface{} { - claims, _ := getClaimsFromToken(r, getSessionToken(r)) + claims, _ := getClaimsFromToken(getSessionToken(r)) return claims } // Fetch claims in the security token returned by the client. -func getClaimsFromToken(r *http.Request, token string) (map[string]interface{}, error) { +func getClaimsFromToken(token string) (map[string]interface{}, error) { claims := xjwt.NewMapClaims() if token == "" { return claims.Map(), nil @@ -237,7 +237,7 @@ func getClaimsFromToken(r *http.Request, token string) (map[string]interface{}, if err != nil { // Base64 decoding fails, we should log to indicate // something is malforming the request sent by client. - logger.LogIf(r.Context(), err, logger.Application) + logger.LogIf(GlobalContext, err, logger.Application) return nil, errAuthentication } claims.MapClaims[iampolicy.SessionPolicyName] = string(spBytes) @@ -258,7 +258,7 @@ func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]in if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 { return nil, ErrInvalidToken } - claims, err := getClaimsFromToken(r, token) + claims, err := getClaimsFromToken(token) if err != nil { return nil, toAPIErrorCode(r.Context(), err) } diff --git a/cmd/bucket-handlers.go b/cmd/bucket-handlers.go index 66ef57f40..49f57f2f5 100644 --- a/cmd/bucket-handlers.go +++ b/cmd/bucket-handlers.go @@ -17,6 +17,7 @@ package cmd import ( + "crypto/subtle" "encoding/base64" "encoding/xml" "fmt" @@ -25,7 +26,6 @@ import ( "net/textproto" "net/url" "path" - "path/filepath" "sort" "strconv" "strings" @@ -337,7 +337,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R // err will be nil here as we already called this function // earlier in this request. - claims, _ := getClaimsFromToken(r, getSessionToken(r)) + claims, _ := getClaimsFromToken(getSessionToken(r)) n := 0 // Use the following trick to filter in place // https://github.com/golang/go/wiki/SliceTricks#filter-in-place @@ -797,13 +797,15 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL, guessIsBrowserReq(r)) return } + resource, err := getResource(r.URL.Path, r.Host, globalDomainNames) if err != nil { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq(r)) return } - // Make sure that the URL does not contain object name. - if bucket != filepath.Clean(resource[1:]) { + + // Make sure that the URL does not contain object name. + if bucket != path.Clean(resource[1:]) { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r)) return } @@ -846,7 +848,6 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h defer fileBody.Close() formValues.Set("Bucket", bucket) - if fileName != "" && strings.Contains(formValues.Get("Key"), "${filename}") { // S3 feature to replace ${filename} found in Key form field // by the filename attribute passed in multipart @@ -866,12 +867,51 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h } // Verify policy signature. - errCode := doesPolicySignatureMatch(formValues) + cred, errCode := doesPolicySignatureMatch(formValues) if errCode != ErrNone { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(errCode), r.URL, guessIsBrowserReq(r)) return } + // Once signature is validated, check if the user has + // explicit permissions for the user. + { + token := formValues.Get(xhttp.AmzSecurityToken) + if token != "" && cred.AccessKey == "" { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoAccessKey), r.URL, guessIsBrowserReq(r)) + return + } + + if cred.IsServiceAccount() && token == "" { + token = cred.SessionToken + } + + if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidToken), r.URL, guessIsBrowserReq(r)) + return + } + + // Extract claims if any. + claims, err := getClaimsFromToken(token) + if err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + + if !globalIAMSys.IsAllowed(iampolicy.Args{ + AccountName: cred.AccessKey, + Action: iampolicy.PutObjectAction, + ConditionValues: getConditionValues(r, "", cred.AccessKey, claims), + BucketName: bucket, + ObjectName: object, + IsOwner: globalActiveCred.AccessKey == cred.AccessKey, + Claims: claims, + }) { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL, guessIsBrowserReq(r)) + return + } + } + policyBytes, err := base64.StdEncoding.DecodeString(formValues.Get("Policy")) if err != nil { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedPOSTRequest), r.URL, guessIsBrowserReq(r)) diff --git a/cmd/signature-v2.go b/cmd/signature-v2.go index 97ca4099e..f0fa6cbd9 100644 --- a/cmd/signature-v2.go +++ b/cmd/signature-v2.go @@ -75,20 +75,18 @@ const ( // AWS S3 Signature V2 calculation rule is give here: // http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationStringToSign - -func doesPolicySignatureV2Match(formValues http.Header) APIErrorCode { - cred := globalActiveCred +func doesPolicySignatureV2Match(formValues http.Header) (auth.Credentials, APIErrorCode) { accessKey := formValues.Get(xhttp.AmzAccessKeyID) cred, _, s3Err := checkKeyValid(accessKey) if s3Err != ErrNone { - return s3Err + return cred, s3Err } policy := formValues.Get("Policy") signature := formValues.Get(xhttp.AmzSignatureV2) if !compareSignatureV2(signature, calculateSignatureV2(policy, cred.SecretKey)) { - return ErrSignatureDoesNotMatch + return cred, ErrSignatureDoesNotMatch } - return ErrNone + return cred, ErrNone } // Escape encodedQuery string into unescaped list of query params, returns error diff --git a/cmd/signature-v2_test.go b/cmd/signature-v2_test.go index 13930709a..bf52c9b71 100644 --- a/cmd/signature-v2_test.go +++ b/cmd/signature-v2_test.go @@ -265,7 +265,7 @@ func TestDoesPolicySignatureV2Match(t *testing.T) { formValues.Set("Awsaccesskeyid", test.accessKey) formValues.Set("Signature", test.signature) formValues.Set("Policy", test.policy) - errCode := doesPolicySignatureV2Match(formValues) + _, errCode := doesPolicySignatureV2Match(formValues) if errCode != test.errCode { t.Fatalf("(%d) expected to get %s, instead got %s", i+1, niceError(test.errCode), niceError(errCode)) } diff --git a/cmd/signature-v4.go b/cmd/signature-v4.go index 757a22092..63ee1cc25 100644 --- a/cmd/signature-v4.go +++ b/cmd/signature-v4.go @@ -39,6 +39,7 @@ import ( "github.com/minio/minio-go/v7/pkg/s3utils" "github.com/minio/minio-go/v7/pkg/set" xhttp "github.com/minio/minio/cmd/http" + "github.com/minio/minio/pkg/auth" ) // AWS Signature Version '4' constants. @@ -149,7 +150,7 @@ func getSignature(signingKey []byte, stringToSign string) string { } // Check to see if Policy is signed correctly. -func doesPolicySignatureMatch(formValues http.Header) APIErrorCode { +func doesPolicySignatureMatch(formValues http.Header) (auth.Credentials, APIErrorCode) { // For SignV2 - Signature field will be valid if _, ok := formValues["Signature"]; ok { return doesPolicySignatureV2Match(formValues) @@ -169,19 +170,19 @@ func compareSignatureV4(sig1, sig2 string) bool { // doesPolicySignatureMatch - Verify query headers with post policy // - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html // returns ErrNone if the signature matches. -func doesPolicySignatureV4Match(formValues http.Header) APIErrorCode { +func doesPolicySignatureV4Match(formValues http.Header) (auth.Credentials, APIErrorCode) { // Server region. region := globalServerRegion // Parse credential tag. credHeader, s3Err := parseCredentialHeader("Credential="+formValues.Get(xhttp.AmzCredential), region, serviceS3) if s3Err != ErrNone { - return s3Err + return auth.Credentials{}, s3Err } cred, _, s3Err := checkKeyValid(credHeader.accessKey) if s3Err != ErrNone { - return s3Err + return cred, s3Err } // Get signing key. @@ -192,11 +193,11 @@ func doesPolicySignatureV4Match(formValues http.Header) APIErrorCode { // Verify signature. if !compareSignatureV4(newSignature, formValues.Get(xhttp.AmzSignature)) { - return ErrSignatureDoesNotMatch + return cred, ErrSignatureDoesNotMatch } // Success. - return ErrNone + return cred, ErrNone } // doesPresignedSignatureMatch - Verify query headers with presigned signature diff --git a/cmd/signature-v4_test.go b/cmd/signature-v4_test.go index b7dc57831..f47d784ec 100644 --- a/cmd/signature-v4_test.go +++ b/cmd/signature-v4_test.go @@ -84,7 +84,7 @@ func TestDoesPolicySignatureMatch(t *testing.T) { // Run each test case individually. for i, testCase := range testCases { - code := doesPolicySignatureMatch(testCase.form) + _, code := doesPolicySignatureMatch(testCase.form) if code != testCase.expected { t.Errorf("(%d) expected to get %s, instead got %s", i, niceError(testCase.expected), niceError(code)) }