mirror of
https://github.com/minio/minio.git
synced 2024-12-24 22:25:54 -05:00
af36c92cab
This is a behavior change from AWS S3, but it is done with better judgment on our end to allow the listing of buckets only which user has access to. The advantage is this declutters the UI for users and only lists bucket which they have access to. Precursor for this feature to be applicable is a policy must have the following actions ``` s3:ListAllMyBuckets ``` and ``` s3:ListBucket ``` enabled in the policy.
521 lines
16 KiB
Go
521 lines
16 KiB
Go
/*
|
|
* MinIO Cloud Storage, (C) 2015-2018 MinIO, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"strings"
|
|
|
|
jwtgo "github.com/dgrijalva/jwt-go"
|
|
xhttp "github.com/minio/minio/cmd/http"
|
|
"github.com/minio/minio/cmd/logger"
|
|
"github.com/minio/minio/pkg/auth"
|
|
"github.com/minio/minio/pkg/hash"
|
|
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
|
"github.com/minio/minio/pkg/policy"
|
|
)
|
|
|
|
// Verify if request has JWT.
|
|
func isRequestJWT(r *http.Request) bool {
|
|
return strings.HasPrefix(r.Header.Get(xhttp.Authorization), jwtAlgorithm)
|
|
}
|
|
|
|
// Verify if request has AWS Signature Version '4'.
|
|
func isRequestSignatureV4(r *http.Request) bool {
|
|
return strings.HasPrefix(r.Header.Get(xhttp.Authorization), signV4Algorithm)
|
|
}
|
|
|
|
// Verify if request has AWS Signature Version '2'.
|
|
func isRequestSignatureV2(r *http.Request) bool {
|
|
return (!strings.HasPrefix(r.Header.Get(xhttp.Authorization), signV4Algorithm) &&
|
|
strings.HasPrefix(r.Header.Get(xhttp.Authorization), signV2Algorithm))
|
|
}
|
|
|
|
// Verify if request has AWS PreSign Version '4'.
|
|
func isRequestPresignedSignatureV4(r *http.Request) bool {
|
|
_, ok := r.URL.Query()[xhttp.AmzCredential]
|
|
return ok
|
|
}
|
|
|
|
// Verify request has AWS PreSign Version '2'.
|
|
func isRequestPresignedSignatureV2(r *http.Request) bool {
|
|
_, ok := r.URL.Query()[xhttp.AmzAccessKeyID]
|
|
return ok
|
|
}
|
|
|
|
// Verify if request has AWS Post policy Signature Version '4'.
|
|
func isRequestPostPolicySignatureV4(r *http.Request) bool {
|
|
return strings.Contains(r.Header.Get(xhttp.ContentType), "multipart/form-data") &&
|
|
r.Method == http.MethodPost
|
|
}
|
|
|
|
// Verify if the request has AWS Streaming Signature Version '4'. This is only valid for 'PUT' operation.
|
|
func isRequestSignStreamingV4(r *http.Request) bool {
|
|
return r.Header.Get(xhttp.AmzContentSha256) == streamingContentSHA256 &&
|
|
r.Method == http.MethodPut
|
|
}
|
|
|
|
// Authorization type.
|
|
type authType int
|
|
|
|
// List of all supported auth types.
|
|
const (
|
|
authTypeUnknown authType = iota
|
|
authTypeAnonymous
|
|
authTypePresigned
|
|
authTypePresignedV2
|
|
authTypePostPolicy
|
|
authTypeStreamingSigned
|
|
authTypeSigned
|
|
authTypeSignedV2
|
|
authTypeJWT
|
|
authTypeSTS
|
|
)
|
|
|
|
// Get request authentication type.
|
|
func getRequestAuthType(r *http.Request) authType {
|
|
if isRequestSignatureV2(r) {
|
|
return authTypeSignedV2
|
|
} else if isRequestPresignedSignatureV2(r) {
|
|
return authTypePresignedV2
|
|
} else if isRequestSignStreamingV4(r) {
|
|
return authTypeStreamingSigned
|
|
} else if isRequestSignatureV4(r) {
|
|
return authTypeSigned
|
|
} else if isRequestPresignedSignatureV4(r) {
|
|
return authTypePresigned
|
|
} else if isRequestJWT(r) {
|
|
return authTypeJWT
|
|
} else if isRequestPostPolicySignatureV4(r) {
|
|
return authTypePostPolicy
|
|
} else if _, ok := r.URL.Query()[xhttp.Action]; ok {
|
|
return authTypeSTS
|
|
} else if _, ok := r.Header[xhttp.Authorization]; !ok {
|
|
return authTypeAnonymous
|
|
}
|
|
return authTypeUnknown
|
|
}
|
|
|
|
// checkAdminRequestAuthType checks whether the request is a valid signature V2 or V4 request.
|
|
// It does not accept presigned or JWT or anonymous requests.
|
|
func checkAdminRequestAuthType(ctx context.Context, r *http.Request, region string) APIErrorCode {
|
|
s3Err := ErrAccessDenied
|
|
if _, ok := r.Header[xhttp.AmzContentSha256]; ok &&
|
|
getRequestAuthType(r) == authTypeSigned && !skipContentSha256Cksum(r) {
|
|
// We only support admin credentials to access admin APIs.
|
|
|
|
var owner bool
|
|
_, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
|
if s3Err != ErrNone {
|
|
return s3Err
|
|
}
|
|
|
|
if !owner {
|
|
return ErrAccessDenied
|
|
}
|
|
|
|
// we only support V4 (no presign) with auth body
|
|
s3Err = isReqAuthenticated(ctx, r, region, serviceS3)
|
|
}
|
|
if s3Err != ErrNone {
|
|
reqInfo := (&logger.ReqInfo{}).AppendTags("requestHeaders", dumpRequest(r))
|
|
ctx := logger.SetReqInfo(ctx, reqInfo)
|
|
logger.LogIf(ctx, errors.New(getAPIError(s3Err).Description))
|
|
}
|
|
return s3Err
|
|
}
|
|
|
|
// Fetch the security token set by the client.
|
|
func getSessionToken(r *http.Request) (token string) {
|
|
token = r.Header.Get(xhttp.AmzSecurityToken)
|
|
if token != "" {
|
|
return token
|
|
}
|
|
return r.URL.Query().Get(xhttp.AmzSecurityToken)
|
|
}
|
|
|
|
// 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)
|
|
return claims
|
|
}
|
|
|
|
// Fetch claims in the security token returned by the client.
|
|
func getClaimsFromToken(r *http.Request) (map[string]interface{}, error) {
|
|
claims := make(map[string]interface{})
|
|
token := getSessionToken(r)
|
|
if token == "" {
|
|
return claims, nil
|
|
}
|
|
stsTokenCallback := func(jwtToken *jwtgo.Token) (interface{}, error) {
|
|
// JWT token for x-amz-security-token is signed with admin
|
|
// secret key, temporary credentials become invalid if
|
|
// server admin credentials change. This is done to ensure
|
|
// that clients cannot decode the token using the temp
|
|
// secret keys and generate an entirely new claim by essentially
|
|
// hijacking the policies. We need to make sure that this is
|
|
// based an admin credential such that token cannot be decoded
|
|
// on the client side and is treated like an opaque value.
|
|
return []byte(globalServerConfig.GetCredential().SecretKey), nil
|
|
}
|
|
p := &jwtgo.Parser{
|
|
ValidMethods: []string{
|
|
jwtgo.SigningMethodHS256.Alg(),
|
|
jwtgo.SigningMethodHS512.Alg(),
|
|
},
|
|
}
|
|
jtoken, err := p.ParseWithClaims(token, jwtgo.MapClaims(claims), stsTokenCallback)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !jtoken.Valid {
|
|
return nil, errAuthentication
|
|
}
|
|
v, ok := claims["accessKey"]
|
|
if !ok {
|
|
return nil, errInvalidAccessKeyID
|
|
}
|
|
if _, ok = v.(string); !ok {
|
|
return nil, errInvalidAccessKeyID
|
|
}
|
|
|
|
if globalPolicyOPA == nil {
|
|
// If OPA is not set, session token should
|
|
// have a policy and its mandatory, reject
|
|
// requests without policy claim.
|
|
p, pok := claims[iampolicy.PolicyName]
|
|
if !pok {
|
|
return nil, errAuthentication
|
|
}
|
|
if _, pok = p.(string); !pok {
|
|
return nil, errAuthentication
|
|
}
|
|
sp, spok := claims[iampolicy.SessionPolicyName]
|
|
// Sub policy is optional, if not set return success.
|
|
if !spok {
|
|
return claims, nil
|
|
}
|
|
// Sub policy is set but its not a string, reject such requests
|
|
spStr, spok := sp.(string)
|
|
if !spok {
|
|
return nil, errAuthentication
|
|
}
|
|
// Looks like subpolicy is set and is a string, if set then its
|
|
// base64 encoded, decode it. Decoding fails reject such requests.
|
|
spBytes, err := base64.StdEncoding.DecodeString(spStr)
|
|
if err != nil {
|
|
// Base64 decoding fails, we should log to indicate
|
|
// something is malforming the request sent by client.
|
|
logger.LogIf(context.Background(), err)
|
|
return nil, errAuthentication
|
|
}
|
|
claims[iampolicy.SessionPolicyName] = string(spBytes)
|
|
}
|
|
return claims, nil
|
|
}
|
|
|
|
// Fetch claims in the security token returned by the client and validate the token.
|
|
func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]interface{}, APIErrorCode) {
|
|
token := getSessionToken(r)
|
|
if token != "" && cred.AccessKey == "" {
|
|
return nil, ErrNoAccessKey
|
|
}
|
|
if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
|
|
return nil, ErrInvalidToken
|
|
}
|
|
claims, err := getClaimsFromToken(r)
|
|
if err != nil {
|
|
return nil, toAPIErrorCode(context.Background(), err)
|
|
}
|
|
return claims, ErrNone
|
|
}
|
|
|
|
// Check request auth type verifies the incoming http request
|
|
// - validates the request signature
|
|
// - validates the policy action if anonymous tests bucket policies if any,
|
|
// for authenticated requests validates IAM policies.
|
|
// returns APIErrorCode if any to be replied to the client.
|
|
func checkRequestAuthType(ctx context.Context, r *http.Request, action policy.Action, bucketName, objectName string) (s3Err APIErrorCode) {
|
|
_, _, s3Err = checkRequestAuthTypeToAccessKey(ctx, r, action, bucketName, objectName)
|
|
return s3Err
|
|
}
|
|
|
|
// Check request auth type verifies the incoming http request
|
|
// - validates the request signature
|
|
// - validates the policy action if anonymous tests bucket policies if any,
|
|
// for authenticated requests validates IAM policies.
|
|
// returns APIErrorCode if any to be replied to the client.
|
|
// Additionally returns the accessKey used in the request, and if this request is by an admin.
|
|
func checkRequestAuthTypeToAccessKey(ctx context.Context, r *http.Request, action policy.Action, bucketName, objectName string) (accessKey string, owner bool, s3Err APIErrorCode) {
|
|
var cred auth.Credentials
|
|
switch getRequestAuthType(r) {
|
|
case authTypeUnknown, authTypeStreamingSigned:
|
|
return accessKey, owner, ErrAccessDenied
|
|
case authTypePresignedV2, authTypeSignedV2:
|
|
if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone {
|
|
return accessKey, owner, s3Err
|
|
}
|
|
cred, owner, s3Err = getReqAccessKeyV2(r)
|
|
case authTypeSigned, authTypePresigned:
|
|
region := globalServerConfig.GetRegion()
|
|
switch action {
|
|
case policy.GetBucketLocationAction, policy.ListAllMyBucketsAction:
|
|
region = ""
|
|
}
|
|
if s3Err = isReqAuthenticated(ctx, r, region, serviceS3); s3Err != ErrNone {
|
|
return accessKey, owner, s3Err
|
|
}
|
|
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
|
}
|
|
if s3Err != ErrNone {
|
|
return accessKey, owner, s3Err
|
|
}
|
|
|
|
var claims map[string]interface{}
|
|
claims, s3Err = checkClaimsFromToken(r, cred)
|
|
if s3Err != ErrNone {
|
|
return accessKey, owner, s3Err
|
|
}
|
|
|
|
// LocationConstraint is valid only for CreateBucketAction.
|
|
var locationConstraint string
|
|
if action == policy.CreateBucketAction {
|
|
// To extract region from XML in request body, get copy of request body.
|
|
payload, err := ioutil.ReadAll(io.LimitReader(r.Body, maxLocationConstraintSize))
|
|
if err != nil {
|
|
logger.LogIf(ctx, err)
|
|
return accessKey, owner, ErrMalformedXML
|
|
}
|
|
|
|
// Populate payload to extract location constraint.
|
|
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
|
|
|
|
var s3Error APIErrorCode
|
|
locationConstraint, s3Error = parseLocationConstraint(r)
|
|
if s3Error != ErrNone {
|
|
return accessKey, owner, s3Error
|
|
}
|
|
|
|
// Populate payload again to handle it in HTTP handler.
|
|
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
|
|
}
|
|
|
|
if cred.AccessKey == "" {
|
|
if globalPolicySys.IsAllowed(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Action: action,
|
|
BucketName: bucketName,
|
|
ConditionValues: getConditionValues(r, locationConstraint, ""),
|
|
IsOwner: false,
|
|
ObjectName: objectName,
|
|
}) {
|
|
// Request is allowed return the appropriate access key.
|
|
return cred.AccessKey, owner, ErrNone
|
|
}
|
|
return accessKey, owner, ErrAccessDenied
|
|
}
|
|
|
|
if globalIAMSys.IsAllowed(iampolicy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Action: iampolicy.Action(action),
|
|
BucketName: bucketName,
|
|
ConditionValues: getConditionValues(r, "", cred.AccessKey),
|
|
ObjectName: objectName,
|
|
IsOwner: owner,
|
|
Claims: claims,
|
|
}) {
|
|
// Request is allowed return the appropriate access key.
|
|
return cred.AccessKey, owner, ErrNone
|
|
}
|
|
return accessKey, owner, ErrAccessDenied
|
|
}
|
|
|
|
// Verify if request has valid AWS Signature Version '2'.
|
|
func isReqAuthenticatedV2(r *http.Request) (s3Error APIErrorCode) {
|
|
if isRequestSignatureV2(r) {
|
|
return doesSignV2Match(r)
|
|
}
|
|
return doesPresignV2SignatureMatch(r)
|
|
}
|
|
|
|
func reqSignatureV4Verify(r *http.Request, region string, stype serviceType) (s3Error APIErrorCode) {
|
|
sha256sum := getContentSha256Cksum(r, stype)
|
|
switch {
|
|
case isRequestSignatureV4(r):
|
|
return doesSignatureMatch(sha256sum, r, region, stype)
|
|
case isRequestPresignedSignatureV4(r):
|
|
return doesPresignedSignatureMatch(sha256sum, r, region, stype)
|
|
default:
|
|
return ErrAccessDenied
|
|
}
|
|
}
|
|
|
|
// Verify if request has valid AWS Signature Version '4'.
|
|
func isReqAuthenticated(ctx context.Context, r *http.Request, region string, stype serviceType) (s3Error APIErrorCode) {
|
|
if errCode := reqSignatureV4Verify(r, region, stype); errCode != ErrNone {
|
|
return errCode
|
|
}
|
|
|
|
var (
|
|
err error
|
|
contentMD5, contentSHA256 []byte
|
|
)
|
|
// Extract 'Content-Md5' if present.
|
|
if _, ok := r.Header[xhttp.ContentMD5]; ok {
|
|
contentMD5, err = base64.StdEncoding.Strict().DecodeString(r.Header.Get(xhttp.ContentMD5))
|
|
if err != nil || len(contentMD5) == 0 {
|
|
return ErrInvalidDigest
|
|
}
|
|
}
|
|
|
|
// Extract either 'X-Amz-Content-Sha256' header or 'X-Amz-Content-Sha256' query parameter (if V4 presigned)
|
|
// Do not verify 'X-Amz-Content-Sha256' if skipSHA256.
|
|
if skipSHA256 := skipContentSha256Cksum(r); !skipSHA256 && isRequestPresignedSignatureV4(r) {
|
|
if sha256Sum, ok := r.URL.Query()[xhttp.AmzContentSha256]; ok && len(sha256Sum) > 0 {
|
|
contentSHA256, err = hex.DecodeString(sha256Sum[0])
|
|
if err != nil {
|
|
return ErrContentSHA256Mismatch
|
|
}
|
|
}
|
|
} else if _, ok := r.Header[xhttp.AmzContentSha256]; !skipSHA256 && ok {
|
|
contentSHA256, err = hex.DecodeString(r.Header.Get(xhttp.AmzContentSha256))
|
|
if err != nil || len(contentSHA256) == 0 {
|
|
return ErrContentSHA256Mismatch
|
|
}
|
|
}
|
|
|
|
// Verify 'Content-Md5' and/or 'X-Amz-Content-Sha256' if present.
|
|
// The verification happens implicit during reading.
|
|
reader, err := hash.NewReader(r.Body, -1, hex.EncodeToString(contentMD5),
|
|
hex.EncodeToString(contentSHA256), -1, globalCLIContext.StrictS3Compat)
|
|
if err != nil {
|
|
return toAPIErrorCode(ctx, err)
|
|
}
|
|
r.Body = ioutil.NopCloser(reader)
|
|
return ErrNone
|
|
}
|
|
|
|
// authHandler - handles all the incoming authorization headers and validates them if possible.
|
|
type authHandler struct {
|
|
handler http.Handler
|
|
}
|
|
|
|
// setAuthHandler to validate authorization header for the incoming request.
|
|
func setAuthHandler(h http.Handler) http.Handler {
|
|
return authHandler{h}
|
|
}
|
|
|
|
// List of all support S3 auth types.
|
|
var supportedS3AuthTypes = map[authType]struct{}{
|
|
authTypeAnonymous: {},
|
|
authTypePresigned: {},
|
|
authTypePresignedV2: {},
|
|
authTypeSigned: {},
|
|
authTypeSignedV2: {},
|
|
authTypePostPolicy: {},
|
|
authTypeStreamingSigned: {},
|
|
}
|
|
|
|
// Validate if the authType is valid and supported.
|
|
func isSupportedS3AuthType(aType authType) bool {
|
|
_, ok := supportedS3AuthTypes[aType]
|
|
return ok
|
|
}
|
|
|
|
// handler for validating incoming authorization headers.
|
|
func (a authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
aType := getRequestAuthType(r)
|
|
if isSupportedS3AuthType(aType) {
|
|
// Let top level caller validate for anonymous and known signed requests.
|
|
a.handler.ServeHTTP(w, r)
|
|
return
|
|
} else if aType == authTypeJWT {
|
|
// Validate Authorization header if its valid for JWT request.
|
|
if _, _, authErr := webRequestAuthenticate(r); authErr != nil {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
a.handler.ServeHTTP(w, r)
|
|
return
|
|
} else if aType == authTypeSTS {
|
|
a.handler.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
writeErrorResponse(context.Background(), w, errorCodes.ToAPIErr(ErrSignatureVersionNotSupported), r.URL, guessIsBrowserReq(r))
|
|
}
|
|
|
|
// isPutAllowed - check if PUT operation is allowed on the resource, this
|
|
// call verifies bucket policies and IAM policies, supports multi user
|
|
// checks etc.
|
|
func isPutAllowed(atype authType, bucketName, objectName string, r *http.Request) (s3Err APIErrorCode) {
|
|
var cred auth.Credentials
|
|
var owner bool
|
|
switch atype {
|
|
case authTypeUnknown:
|
|
return ErrAccessDenied
|
|
case authTypeSignedV2, authTypePresignedV2:
|
|
cred, owner, s3Err = getReqAccessKeyV2(r)
|
|
case authTypeStreamingSigned, authTypePresigned, authTypeSigned:
|
|
region := globalServerConfig.GetRegion()
|
|
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
|
}
|
|
if s3Err != ErrNone {
|
|
return s3Err
|
|
}
|
|
|
|
claims, s3Err := checkClaimsFromToken(r, cred)
|
|
if s3Err != ErrNone {
|
|
return s3Err
|
|
}
|
|
|
|
if cred.AccessKey == "" {
|
|
if globalPolicySys.IsAllowed(policy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Action: policy.PutObjectAction,
|
|
BucketName: bucketName,
|
|
ConditionValues: getConditionValues(r, "", ""),
|
|
IsOwner: false,
|
|
ObjectName: objectName,
|
|
}) {
|
|
return ErrNone
|
|
}
|
|
return ErrAccessDenied
|
|
}
|
|
|
|
if globalIAMSys.IsAllowed(iampolicy.Args{
|
|
AccountName: cred.AccessKey,
|
|
Action: policy.PutObjectAction,
|
|
BucketName: bucketName,
|
|
ConditionValues: getConditionValues(r, "", cred.AccessKey),
|
|
ObjectName: objectName,
|
|
IsOwner: owner,
|
|
Claims: claims,
|
|
}) {
|
|
return ErrNone
|
|
}
|
|
return ErrAccessDenied
|
|
}
|