cubefs/objectnode/auth.go
yhjiango e487250c51 fix(object): adjust STS auth process
Signed-off-by: yhjiango <jiangyunhua@oppo.com>

(cherry picked from commit 1f23b3f620)
Signed-off-by: yhjiango <jiangyunhua@oppo.com>
2024-07-18 16:57:38 +08:00

315 lines
11 KiB
Go

// Copyright 2019 The CubeFS Authors.
//
// 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 objectnode
import (
"net/http"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/cubefs/cubefs/proto"
"github.com/cubefs/cubefs/util/log"
)
// https://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#ConstructingTheAuthenticationHeader
var (
ErrMalformedDate = &ErrorCode{
ErrorCode: "AuthorizationParameterError",
ErrorMessage: "Your request date format is invalid, expected to be in ISO8601, RFC1123 or RFC1123Z time format.",
StatusCode: http.StatusBadRequest,
}
ErrMalformedCredential = &ErrorCode{
ErrorCode: "AuthorizationParameterError",
ErrorMessage: "The credential is mal-formed, expecting \"<your ak>/<yyyyMMdd>/<region>/<service>/aws4_request\".",
StatusCode: http.StatusBadRequest,
}
ErrMalformedXAmzDate = &ErrorCode{
ErrorCode: "AuthorizationParameterError",
ErrorMessage: "X-Amz-Date should be in the ISO8601 Long Format \"yyyyMMdd'T'HHmmss'Z'\".",
StatusCode: http.StatusBadRequest,
}
ErrInvalidAuthHeader = &ErrorCode{
ErrorCode: "AuthorizationHeaderError",
ErrorMessage: "The authorization header that you provided is not valid.",
StatusCode: http.StatusBadRequest,
}
ErrMissingSignedHeaders = &ErrorCode{
ErrorCode: "AuthorizationHeaderError",
ErrorMessage: "SignedHeaders field is missing from the authentication header you provided.",
StatusCode: http.StatusBadRequest,
}
ErrMissingSignature = &ErrorCode{
ErrorCode: "AuthorizationHeaderError",
ErrorMessage: "Signature field is missing from the authentication header you provided.",
StatusCode: http.StatusBadRequest,
}
ErrMissingDateHeader = &ErrorCode{
ErrorCode: "AuthorizationHeaderError",
ErrorMessage: "Your request is missing a valid Date or X-Amz-Date parameter in header.",
StatusCode: http.StatusBadRequest,
}
ErrMalformedPOSTRequest = &ErrorCode{
ErrorCode: "AuthorizationFormError",
ErrorMessage: "The body of your POST request is not well-formed multipart/form-data.",
StatusCode: http.StatusBadRequest,
}
ErrMissingFormFields = &ErrorCode{
ErrorCode: "AuthorizationFormError",
ErrorMessage: "The body of your POST request is missing required authentication fields.",
StatusCode: http.StatusBadRequest,
}
ErrFormMissingDateParam = &ErrorCode{
ErrorCode: "AuthorizationFormError",
ErrorMessage: "Your request is missing a valid X-Amz-Date in body or Date in header.",
StatusCode: http.StatusBadRequest,
}
ErrMissingAuthQueryParams = &ErrorCode{
ErrorCode: "AuthorizationQueryError",
ErrorMessage: "Missing required authorization fields in the query parameters.",
StatusCode: http.StatusBadRequest,
}
ErrInvalidAuthQueryParams = &ErrorCode{
ErrorCode: "AuthorizationQueryError",
ErrorMessage: "The query parameters that provide authentication information is not valid.",
StatusCode: http.StatusBadRequest,
}
ErrMalformedQueryExpires = &ErrorCode{
ErrorCode: "AuthorizationQueryError",
ErrorMessage: "The expires parameter should be a number.",
StatusCode: http.StatusBadRequest,
}
ErrInvalidRangeExpires = &ErrorCode{
ErrorCode: "AuthorizationQueryError",
ErrorMessage: "The expires parameter should be within a valid range.",
StatusCode: http.StatusBadRequest,
}
)
const (
AWSAccessKeyID = "AWSAccessKeyId"
signatureV2 = "AWS"
signatureV4 = "AWS4"
signV2Algorithm = "HMAC-SHA1"
signV4Algorithm = "AWS4-HMAC-SHA256"
streamingContentEncoding = "aws-chunked"
credentialFlag = "Credential=" // #nosec G101
signatureFlag = "Signature="
signedHeadersFlag = "SignedHeaders="
MaxSignatureExpires = 7 * 24 * 60 * 60 // 7 days
MinSignatureExpires = 1 // 1 second
MaxRequestSkewedSeconds = 15 * 60 // 15 min
UnsignedPayload = "UNSIGNED-PAYLOAD"
EmptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
)
type Auther interface {
IsPresigned() bool
IsExpired() bool
IsSkewed() bool
Version() string
Algorithm() string
Signature() string
Credential() *Credential
SignedHeaders() []string
StringToSign() string
CanonicalRequest() string
SignatureMatch(string, Wildcards) bool
}
type SignatureInfo struct {
credential *Credential // parsed auth credential
signedHeaders []string // parsed name of signed headers, only available for AWS4
version string // parsed auth version, e.g. AWS4
algorithm string // parsed auth algorithm, e.g. AWS4-HMAC-SHA256
signature string // requested signature
stringToSign string // string to sign, used to calculate the signature
canonicalRequest string // canonical request, used to calculate the signature
}
func NewAuth(r *http.Request) (Auther, error) {
switch {
case r.Header.Get(Authorization) != "":
return NewHeaderAuth(r)
case r.URL.Query().Get(Signature) != "" || r.URL.Query().Get(XAmzSignature) != "":
return NewQueryAuth(r)
case r.Method == http.MethodPost && strings.Contains(r.Header.Get(ContentType), ValueMultipartFormData):
return NewFormAuth(r)
default:
return nil, MissingSecurityElement
}
}
type Credential struct {
Region string // e.g. cfs-dev
Service string // e.g. s3
Request string // e.g. aws4_request
Date string // e.g. 20060102
TimeStamp string // e.g. 20060102T150405Z
Expires int64 // seconds, e.g. 86400
AccessKey string // e.g. AKCubeFS2EXAMPLE
}
func (c *Credential) ParseTimeStamp() (time.Time, error) {
t, err := ParseCompatibleTime(c.TimeStamp)
if err != nil {
return t, err
}
return t.UTC(), nil
}
func (c *Credential) ParseDate() (time.Time, error) {
return time.Parse(DateLayout, c.Date)
}
func (o *ObjectNode) validateAuthInfo(r *http.Request, auth Auther) (err error) {
param := ParseRequestParam(r)
reqAK := auth.Credential().AccessKey
var uid, ak, sk, token string
var stsInfo *FedDecodeResult
if token = getSecurityToken(r); token == "" {
ak = reqAK
if uid, sk, err = o.getUidSecretKeyWithCheckVol(r, ak, true); err != nil {
log.LogErrorf("validateAuthInfo: get user uid and sk fail: requestID(%v) ak(%v) err(%v)",
GetRequestID(r), ak, err)
return err
}
} else {
stsInfo, err = DecodeFedSessionToken(reqAK, token, o.getUserInfoByAccessKeyV2)
if err != nil {
log.LogErrorf("validateAuthInfo: decode session token fail: requestID(%v) ak(%v) token(%v) err(%v)",
GetRequestID(r), reqAK, token, err)
return err
}
uid, ak, sk = stsInfo.UserInfo.UserID, stsInfo.UserInfo.AccessKey, stsInfo.FedSK
}
mux.Vars(r)[ContextKeyAccessKey] = ak
mux.Vars(r)[ContextKeyRequester] = uid
if !o.signatureIgnoredActions.Contains(param.action) {
cred := auth.Credential()
if auth.IsSkewed() {
log.LogErrorf("validateAuthInfo: request skewed: requestID(%v) reqTime(%v) servTime(%v)",
GetRequestID(r), cred.TimeStamp, time.Now().UTC().Format(ISO8601Format))
return RequestTimeTooSkewed
}
if auth.IsExpired() {
log.LogErrorf("validateAuthInfo: signature has expired: requestID(%v) servTime(%v) reqDate(%v) expires(%v)",
GetRequestID(r), time.Now().UTC().Format(ISO8601Format), cred.Date, cred.Expires)
return ExpiredToken
}
if !auth.SignatureMatch(sk, o.wildcards) {
log.LogErrorf("validateAuthInfo: signature not match: requestID(%v) AccessKeyId(%v)\nstringToSign=(\n%v\n)\ncanonialRequest=(\n%v\n)",
GetRequestID(r), reqAK, auth.StringToSign(), auth.CanonicalRequest())
return SignatureDoesNotMatch
}
}
if stsInfo != nil {
if o.stsNotAllowedActions.Contains(param.action) {
log.LogErrorf("validateAuthInfo: action not allowed by sts user: requestID(%v) action(%v)",
GetRequestID(r), param.action)
return AccessDeniedBySTS
}
userPolicy := stsInfo.UserInfo.Policy
if !IsAccountLevelApi(param.apiName) && param.bucket != "" && userPolicy != nil && !userPolicy.IsOwn(param.
bucket) {
log.LogErrorf("validateAuthInfo: sts user access non-owner vol: requestID(%v) reqVol(%v) ownVols(%v)",
GetRequestID(r), param.bucket, userPolicy.OwnVols)
return AccessDenied
}
action := "s3:" + param.apiName
if !stsInfo.Policy.IsAllow(action, param.bucket, param.object) {
log.LogErrorf("validateAuthInfo: sts policy not allow: requestID(%v) policy(%v) api(%v) resource(%v)",
GetRequestID(r), *stsInfo.Policy, param.apiName, param.resource)
return AccessDenied
}
}
if stsInfo != nil {
if o.stsNotAllowedActions.Contains(param.action) {
log.LogErrorf("validateAuthInfo: action not allowed by sts user: requestID(%v) action(%v)",
GetRequestID(r), param.action)
return AccessDeniedBySTS
}
userPolicy := stsInfo.UserInfo.Policy
if !IsAccountLevelApi(param.apiName) && param.bucket != "" && userPolicy != nil && !userPolicy.IsOwn(param.
bucket) {
log.LogErrorf("validateAuthInfo: sts user access non-owner vol: requestID(%v) reqVol(%v) ownVols(%v)",
GetRequestID(r), param.bucket, userPolicy.OwnVols)
return AccessDenied
}
action := "s3:" + param.apiName
if !stsInfo.Policy.IsAllow(action, param.bucket, param.object) {
log.LogErrorf("validateAuthInfo: sts policy not allow: requestID(%v) policy(%v) api(%v) resource(%v)",
GetRequestID(r), *stsInfo.Policy, param.apiName, param.resource)
return AccessDenied
}
}
return nil
}
func (o *ObjectNode) getUidSecretKeyWithCheckVol(r *http.Request, ak string, ck bool) (uid, sk string, err error) {
info, err := o.getUserInfoByAccessKey(ak)
if err == nil {
uid, sk = info.UserID, info.SecretKey
return
}
if err == proto.ErrUserNotExists || err == proto.ErrAccessKeyNotExists || err == proto.ErrParamError {
bucket := mux.Vars(r)[ContextKeyBucket]
if len(bucket) > 0 && ck && GetActionFromContext(r) != proto.OSSCreateBucketAction {
// In order to be directly compatible with the signature verification of version 1.5
// (each volume has its own access key and secret key), if the user does not exist and
// the request specifies a volume, try to use the access key and secret key bound in the
// volume information for verification.
var vol *Volume
if vol, err = o.getVol(bucket); err != nil {
return
}
if ossAk, ossSk := vol.OSSSecure(); ossAk == ak {
uid, sk = vol.GetOwner(), ossSk
return
}
}
err = InvalidAccessKeyId
}
return
}
func getSecurityToken(r *http.Request) string {
if token := r.Header.Get(XAmzSecurityToken); token != "" {
return token
}
if token := r.URL.Query().Get(XAmzSecurityToken); token != "" {
return token
}
isFormUpload := r.Method == http.MethodPost && strings.Contains(r.Header.Get(ContentType), ValueMultipartFormData)
if !isFormUpload {
return ""
}
// ParseMultipartForm is assumed to have been called
return r.Form.Get(XAmzSecurityToken)
}