diff --git a/objectnode/acl.go b/objectnode/acl.go index 591ec1248..9255df639 100644 --- a/objectnode/acl.go +++ b/objectnode/acl.go @@ -190,7 +190,7 @@ func (acp *AccessControlPolicy) Validate(bucket string) (bool, error) { } func (acp *AccessControlPolicy) IsAllowed(param *RequestParam, isOwner bool) bool { - log.LogDebugf("acl is allowed: %v param: %v", acp, param) + log.LogDebugf("start to check acl(%v) requestID(%v)", acp, GetRequestID(param.r)) if len(acp.Acl.Grants) == 0 { return true } diff --git a/objectnode/api_handler.go b/objectnode/api_handler.go index e909294e0..3b6968d89 100644 --- a/objectnode/api_handler.go +++ b/objectnode/api_handler.go @@ -20,22 +20,21 @@ import ( "strings" "github.com/cubefs/cubefs/proto" + "github.com/cubefs/cubefs/util/log" "github.com/gorilla/mux" - - "github.com/cubefs/cubefs/util/log" ) type RequestParam struct { - resource string - bucket string - object string - action proto.Action - sourceIP string - conditionVars map[string][]string - vars map[string]string - accessKey string - r *http.Request + resource string + bucket string + object string + action proto.Action + apiName string + sourceIP string + vars map[string]string + accessKey string + r *http.Request } func (p *RequestParam) Bucket() string { @@ -57,10 +56,6 @@ func (p *RequestParam) GetVar(name string) string { return p.r.FormValue(name) } -func (p *RequestParam) GetConditionVar(name string) []string { - return p.conditionVars[name] -} - func (p *RequestParam) AccessKey() string { return p.accessKey } @@ -72,7 +67,6 @@ func ParseRequestParam(r *http.Request) *RequestParam { p.bucket = p.vars["bucket"] p.object = p.vars["object"] p.sourceIP = getRequestIP(r) - p.conditionVars = getCondtionValues(r) if len(p.bucket) > 0 { p.resource = p.bucket if len(p.object) > 0 { @@ -91,6 +85,7 @@ func ParseRequestParam(r *http.Request) *RequestParam { if p.action.IsNone() { p.action = ActionFromRouteName(mux.CurrentRoute(r).GetName()) } + p.apiName = strings.TrimPrefix(string(p.action), proto.OSSActionPrefix) return p } diff --git a/objectnode/api_handler_object.go b/objectnode/api_handler_object.go index 727f7deaa..25695648e 100644 --- a/objectnode/api_handler_object.go +++ b/objectnode/api_handler_object.go @@ -595,8 +595,34 @@ func (o *ObjectNode) deleteObjectsHandler(w http.ResponseWriter, r *http.Request return deleteReq.Objects[i].Key > deleteReq.Objects[j].Key }) + vol, _, policy, vv, err := o.loadBucketMeta(param.Bucket()) + if err != nil { + log.LogErrorf("get policy : load bucket metadata fail: requestID(%v) err(%v)", GetRequestID(r), err) + return + } + + userInfo, err := o.getUserInfoByAccessKey(param.AccessKey()) + if err != nil { + log.LogErrorf("get userinfo fail: requestID(%v) err(%v)", GetRequestID(r), err) + return + } var objectKeys = make([]string, 0, len(deleteReq.Objects)) for _, object := range deleteReq.Objects { + if policy != nil && !policy.IsEmpty() { + log.LogDebugf("bucket policy check: requestID(%v) policy(%v)", GetRequestID(r), policy) + conditionCheck := map[string]string{ + SOURCEIP: param.sourceIP, + REFERER: param.r.Referer(), + KEYNAME: object.Key, + HOST: param.r.Host, + } + pcr := policy.IsAllowed(param, userInfo.UserID, vv.Owner, conditionCheck) + // add acl check + if pcr == POLICY_DENY { + deletedErrors = append(deletedErrors, Error{Key: object.Key, Message: "AccessDenied"}) + continue + } + } objectKeys = append(objectKeys, object.Key) err = vol.DeletePath(object.Key) log.LogWarnf("deleteObjectsHandler: delete: requestID(%v) volume(%v) path(%v)", @@ -641,26 +667,30 @@ func (o *ObjectNode) deleteObjectsHandler(w http.ResponseWriter, r *http.Request return } -func parseCopySourceInfo(r *http.Request) (sourceBucket, sourceObject string) { - var copySource = r.Header.Get(HeaderNameXAmzCopySource) - if s, err := url.PathUnescape(copySource); err == nil { - copySource = s - } - if strings.HasPrefix(copySource, "/") { - copySource = copySource[1:] +func extractSrcBucketKey(r *http.Request) (srcBucketId, srcKey, versionId string, err error) { + copySource := r.Header.Get(HeaderNameXAmzCopySource) + copySource, err = url.QueryUnescape(copySource) + if err != nil { + return "", "", "", InvalidArgument } - position := strings.Index(copySource, "/") - var bucket, object string - if position >= 0 { - bucket = copySource[:position] - if position+1 <= len(copySource) { - object = copySource[position+1:] - } + // path could be /bucket/key or bucket/key + copySource = strings.TrimPrefix(copySource, "/") + elements := strings.SplitN(copySource, "?versionId=", 2) + if len(elements) == 1 { + versionId = "" + } else { + versionId = elements[1] + } + path := strings.SplitN(elements[0], "/", 2) + if len(path) == 1 { + return "", "", "", InvalidArgument } - sourceBucket = bucket - sourceObject = object + srcBucketId, srcKey = path[0], path[1] + if srcBucketId == "" || srcKey == "" { + return "", "", "", InvalidArgument + } return } @@ -725,7 +755,11 @@ func (o *ObjectNode) copyObjectHandler(w http.ResponseWriter, r *http.Request) { Expires: expires, } - sourceBucket, sourceObject := parseCopySourceInfo(r) + sourceBucket, sourceObject, _, err := extractSrcBucketKey(r) + if err != nil { + log.LogDebugf("copySource(%v) argument invalid: requestID(%v)", r.Header.Get(HeaderNameXAmzCopySource), GetRequestID(r)) + return + } // check permission, must have read permission to source bucket var userInfo *proto.UserInfo diff --git a/objectnode/api_middleware.go b/objectnode/api_middleware.go index f200923f0..96f1bbba9 100644 --- a/objectnode/api_middleware.go +++ b/objectnode/api_middleware.go @@ -185,7 +185,8 @@ func (o *ObjectNode) authMiddleware(next http.Handler) http.Handler { _ = InternalErrorCode(err).ServeResponse(w, r) return } - if !pass { + authInfo := parseRequestAuthInfo(r) + if !pass && !isAnonymous(authInfo.accessKey) { _ = AccessDenied.ServeResponse(w, r) return } diff --git a/objectnode/fs_volume.go b/objectnode/fs_volume.go index 6c8c35edc..dd06e9481 100644 --- a/objectnode/fs_volume.go +++ b/objectnode/fs_volume.go @@ -2622,7 +2622,7 @@ func (v *Volume) CopyFile(sv *Volume, sourcePath, targetPath, metaDirective stri var xattr *proto.XAttrInfo // if source path is same with target path, just reset file metadata // source path is same with target path, and metadata directive is not 'REPLACE', object node do nothing - if targetPath == sourcePath { + if targetPath == sourcePath && v.name == sv.name { if metaDirective != MetadataDirectiveReplace { log.LogInfof("CopyFile: target path is equal with source path, object node do nothing, source path(%v) target path(%v) err(%v)", sourcePath, targetPath, err) diff --git a/objectnode/policy.go b/objectnode/policy.go index 57bcee0ad..402435fab 100644 --- a/objectnode/policy.go +++ b/objectnode/policy.go @@ -22,23 +22,34 @@ package objectnode import ( "encoding/json" "errors" - "io" "net/http" "strings" - "github.com/gorilla/mux" - "github.com/cubefs/cubefs/proto" "github.com/cubefs/cubefs/util/log" -) -type ActionType string + "github.com/gorilla/mux" +) // https://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html const ( - PolicyDefaultVersion = "2012-10-17" BucketPolicyLimitSize = 20 * 1024 //Bucket policies are limited to 20KB - ArnSplitToken = ":" + maxStatementNum = 10 +) + +var ( + ErrMissingVersionInPolicy = &ErrorCode{ErrorCode: "ErrMissingVersionInPolicy", ErrorMessage: "missing Version in policy", StatusCode: http.StatusBadRequest} + ErrMissingStatementInPolicy = &ErrorCode{ErrorCode: "MissingStatementInPolicy", ErrorMessage: "missing Statement in policy", StatusCode: http.StatusBadRequest} + ErrMissingEffectInPolicy = &ErrorCode{ErrorCode: "MissingEffectInPolicy", ErrorMessage: "missing Effect in policy", StatusCode: http.StatusBadRequest} + ErrMissingPrincipalInPolicy = &ErrorCode{ErrorCode: "MissingPrincipalInPolicy", ErrorMessage: "missing Principal in policy", StatusCode: http.StatusBadRequest} + ErrMissingActionInPolicy = &ErrorCode{ErrorCode: "MissingActionInPolicy", ErrorMessage: "missing Action in policy", StatusCode: http.StatusBadRequest} + ErrMissingResourceInPolicy = &ErrorCode{ErrorCode: "MissingResourceInPolicy", ErrorMessage: "missing Resource in policy", StatusCode: http.StatusBadRequest} + ErrTooManyStatementInPolicy = &ErrorCode{ErrorCode: "TooManyStatementInPolicy", ErrorMessage: "too many statement in policy", StatusCode: http.StatusBadRequest} + ErrInvalidEffectValue = &ErrorCode{ErrorCode: "InvalidEffectValue", ErrorMessage: "Effect can only be Allow or Deny", StatusCode: http.StatusBadRequest} + ErrInvalidPricipalInPolicy = &ErrorCode{ErrorCode: "InvalidPricipalInPolicy", ErrorMessage: "Invalid Principal in policy", StatusCode: http.StatusBadRequest} + ErrInvalidActionInPolicy = &ErrorCode{ErrorCode: "InvalidActionInPolicy", ErrorMessage: "Invalid Action in policy", StatusCode: http.StatusBadRequest} + ErrInvalidResourceInPolicy = &ErrorCode{ErrorCode: "InvalidResourceInPolicy", ErrorMessage: "Invalid Resource in policy", StatusCode: http.StatusBadRequest} + ErrInvalidActionResourceCombination = &ErrorCode{ErrorCode: "InvalidActionResourceCombination", ErrorMessage: "Action does not apply to any resource in statement", StatusCode: http.StatusBadRequest} ) //https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/example-bucket-policies.html @@ -53,42 +64,6 @@ func (p *Policy) IsEmpty() bool { return len(p.Statements) == 0 } -// arn:partition:service:region:account-id:resource-id -// arn:partition:service:region:account-id:resource-type/resource-id -// arn:partition:service:region:account-id:resource-type:resource-id -type Arn struct { - arn Resource - partition string //aws - service string //s3/iam - region string // - accountId string // - resourceType string // - resourceId string // -} - -func parseArn(str string) (*Arn, error) { - items := strings.Split(str, ArnSplitToken) - if len(items) < 4 { - log.LogErrorf("Arn is invalid: %v", str) - return nil, errors.New("invalid arn") - } - arn := &Arn{ - partition: items[1], - service: items[2], - region: items[3], - accountId: items[4], - } - - if len(items) > 6 { - arn.resourceType = items[5] - arn.resourceId = items[6] - } else { - arn.resourceId = items[5] - } - - return arn, nil -} - // write bucket policy into store and update vol policy meta func storeBucketPolicy(bytes []byte, vol *Volume) (*Policy, error) { policy := &Policy{} @@ -126,30 +101,21 @@ func deleteBucketPolicy(vol *Volume) (err error) { return nil } -func ParsePolicy(r io.Reader, bucket string) (*Policy, error) { - var policy Policy - d := json.NewDecoder(r) - d.DisallowUnknownFields() - if err := d.Decode(&policy); err != nil { - return nil, err - } - - if ok, err := policy.Validate(bucket); !ok { - return nil, err - } - - return &policy, nil -} - func (p Policy) isValid() (bool, error) { if p.Version == "" { - return false, errors.New("policy version cannot be empty") + return false, ErrMissingVersionInPolicy + } + if len(p.Statements) == 0 { + return false, ErrMissingStatementInPolicy + } + if len(p.Statements) > maxStatementNum { + return false, ErrTooManyStatementInPolicy } - return true, nil } func (p Policy) Validate(bucket string) (bool, error) { + log.LogDebug("check policy syntax") if ok, err1 := p.isValid(); !ok { return false, err1 } @@ -165,33 +131,29 @@ func (p Policy) Validate(bucket string) (bool, error) { // check policy is allowed for request // https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/reference_policies_evaluation-logic.html -func (p *Policy) IsAllowed(params *RequestParam, isOwner bool) bool { - for _, s := range p.Statements { - if s.Effect == Deny { - if !s.IsAllowed(params) { - log.LogDebugf("policy deny cause of %v, %v", s, params) - return false - } +func (p *Policy) IsAllowed(params *RequestParam, reqUid, ownerUid string, conditionCheck map[string]string) PolicyCheckResult { + result := POLICY_UNKNOW + apiName := params.apiName + // only bucket owner is allowed to put/get/delete bucket policy + if isPolicyApi(apiName) { + if reqUid == ownerUid { + return POLICY_ALLOW + } + return POLICY_DENY + } + if !supportByPolicy(apiName) { + return POLICY_UNKNOW + } + for _, statement := range p.Statements { + if tmp := statement.CheckPolicy(apiName, reqUid, conditionCheck); tmp == POLICY_DENY { + log.LogDebugf("bucket policy check: statement denied: requestID(%v) statement(%v)", GetRequestID(params.r), statement) + return POLICY_DENY + } else if tmp == POLICY_ALLOW { + log.LogDebugf("bucket policy check: statement allowed: requestID(%v) statement(%v)", GetRequestID(params.r), statement) + result = POLICY_ALLOW } } - - //is owner - if isOwner { - return true - } - - for _, s := range p.Statements { - if s.Effect == Allow { - if s.IsAllowed(params) { - log.LogDebugf("policy allow cause of %v, %v", s, params) - return true - } - } - } - - log.LogDebugf("policy deny cause of %v, request: %v", p, params) - - return false + return result } func (o *ObjectNode) policyCheck(f http.HandlerFunc) http.HandlerFunc { @@ -213,19 +175,21 @@ func (o *ObjectNode) policyCheck(f http.HandlerFunc) http.HandlerFunc { }() param := ParseRequestParam(r) - if param.Bucket() == "" { log.LogDebugf("policyCheck: no bucket specified: requestID(%v)", GetRequestID(r)) allowed = true return } - // A create bucket action do not need to check any user policy and volume policy. - if param.action == proto.OSSCreateBucketAction { - allowed = true + // step1. The account level api does not need to check any user policy and volume policy. + if IsAccountLevelApi(param.apiName) { + if !isAnonymous(param.accessKey) { + allowed = true + return + } + allowed = false return } - // Check user policy var volume *Volume if bucket := mux.Vars(r)["bucket"]; len(bucket) > 0 { if volume, err = o.getVol(bucket); err != nil { @@ -238,12 +202,23 @@ func (o *ObjectNode) policyCheck(f http.HandlerFunc) http.HandlerFunc { return } } - var userInfo *proto.UserInfo + + //step2. Check user policy + userInfo := new(proto.UserInfo) isOwner := false + if isAnonymous(param.accessKey) && apiAllowAnonymous(param.apiName) { + log.LogDebugf("anonymous user: requestID(%v)", GetRequestID(r)) + goto policycheck + } + if isAnonymous(param.accessKey) && !apiAllowAnonymous(param.apiName) { + log.LogDebugf("anonymous user is not allowed by api(%v) requestID(%v)", param.apiName, GetRequestID(r)) + allowed = false + return + } if userInfo, err = o.getUserInfoByAccessKey(param.AccessKey()); err == nil { // White list for admin and root user. if userInfo.UserType == proto.UserTypeRoot || userInfo.UserType == proto.UserTypeAdmin { - log.LogDebugf("policyCheck: user is admin: requestID(%v) userID(%v) accessKey(%v) volume(%v)", + log.LogDebugf("user policy check: user is admin: requestID(%v) userID(%v) accessKey(%v) volume(%v)", GetRequestID(r), userInfo.UserID, param.AccessKey(), param.Bucket()) allowed = true return @@ -254,11 +229,10 @@ func (o *ObjectNode) policyCheck(f http.HandlerFunc) http.HandlerFunc { if subdir == "" { subdir = r.URL.Query().Get(ParamPrefix) } + // The bucket is not owned by request user who has not been authorized, so bucket policy should be checked. if !isOwner && !userPolicy.IsAuthorized(param.Bucket(), subdir, param.Action()) { - log.LogDebugf("policyCheck: user no permission: url(%v) subdir(%v) requestID(%v) userID(%v) accessKey(%v) volume(%v) object(%v) action(%v)", - r.URL, subdir, GetRequestID(r), userInfo.UserID, param.AccessKey(), param.Bucket(), param.Object(), param.Action()) - allowed = false - return + log.LogDebugf("user policy check: permission unknown url(%v) subdir(%v) requestID(%v) userID(%v) accessKey(%v) volume(%v) object(%v) action(%v) authorizedVols(%v)", + r.URL, subdir, GetRequestID(r), userInfo.UserID, param.AccessKey(), param.Bucket(), param.Object(), param.Action(), userPolicy.AuthorizedVols) } } else if (err == proto.ErrAccessKeyNotExists || err == proto.ErrUserNotExists) && volume != nil { if ak, _ := volume.OSSSecure(); ak != param.AccessKey() { @@ -267,53 +241,142 @@ func (o *ObjectNode) policyCheck(f http.HandlerFunc) http.HandlerFunc { } isOwner = true } else { - log.LogErrorf("policyCheck: load user policy from master fail: requestID(%v) accessKey(%v) err(%v)", + log.LogErrorf("user policy check: load user policy from master fail: requestID(%v) accessKey(%v) err(%v)", GetRequestID(r), param.AccessKey(), err) allowed = false return } - var vol *Volume - var acl *AccessControlPolicy - var policy *Policy - var loadBucketMeta = func(bucket string) (err error) { - if vol, err = o.getVol(bucket); err != nil { + // copy api should check srcBucket policy additionally + if param.apiName == COPY_OBJECT || param.apiName == UPLOAD_PART_COPY { + err := o.allowedBySrcBucketPolicy(param, userInfo.UserID) + if err != nil { return } - if acl, err = vol.metaLoader.loadACL(); err != nil { - return - } - if policy, err = vol.metaLoader.loadPolicy(); err != nil { - return - } - return } - if err = loadBucketMeta(param.Bucket()); err != nil { - log.LogErrorf("policyCheck: load bucket metadata fail: requestID(%v) err(%v)", GetRequestID(r), err) + + //step3. Check bucket policy + policycheck: + vol, acl, policy, vv, err := o.loadBucketMeta(param.Bucket()) + if err != nil { + log.LogErrorf("bucket policy check: load bucket metadata fail: requestID(%v) err(%v)", GetRequestID(r), err) allowed = false ec = NoSuchBucket return } if vol != nil && policy != nil && !policy.IsEmpty() { - allowed = policy.IsAllowed(param, isOwner) - if !allowed { - log.LogWarnf("policyCheck: bucket policy not allowed: requestID(%v) userID(%v) accessKey(%v) volume(%v) action(%v)", - GetRequestID(r), userInfo, param.AccessKey(), param.Bucket(), param.Action()) + log.LogDebugf("bucket policy check: requestID(%v) policy(%v)", GetRequestID(r), policy) + conditionCheck := map[string]string{ + SOURCEIP: param.sourceIP, + REFERER: param.r.Referer(), + HOST: param.r.Host, + } + if !IsBucketApi(param.apiName) { + conditionCheck[KEYNAME] = param.object + } + pcr := policy.IsAllowed(param, userInfo.UserID, vv.Owner, conditionCheck) + switch pcr { + case POLICY_ALLOW: + allowed = true + log.LogDebugf("bucket policy check: policy allowed: requestID(%v)", GetRequestID(r)) return + case POLICY_DENY: + allowed = false + log.LogWarnf("bucket policy check: policy not allowed: requestID(%v) ", GetRequestID(r)) + return + case POLICY_UNKNOW: + // policy check result is unknown so that acl should be checked + log.LogWarnf("bucket policy check: policy unknown: requestID(%v) ", GetRequestID(r)) } } - if vol != nil && acl != nil && !acl.IsAclEmpty() { + + //step4. Check acl + //The default bucket acl should not be empty + if acl == nil && !isOwner { + allowed = false + log.LogWarnf("bucket acl check: not allowed because of empty bucket ACL : requestID(%v) ", GetRequestID(r)) + return + } + if acl != nil && !acl.IsAclEmpty() { allowed = acl.IsAllowed(param, isOwner) if !allowed { - log.LogWarnf("policyCheck: bucket ACL not allowed: requestID(%v) userID(%v) accessKey(%v) volume(%v) action(%v)", - GetRequestID(r), userInfo, param.AccessKey(), param.Bucket(), param.Action()) + log.LogWarnf("bucket acl check: bucket ACL not allowed: requestID(%v) acl(%v) accessKey(%v) volume(%v) action(%v)", + GetRequestID(r), userInfo, acl, param.Bucket(), param.Action()) return } } allowed = true - log.LogDebugf("policyCheck: action allowed: requestID(%v) userID(%v) accessKey(%v) volume(%v) action(%v)", + log.LogDebugf("bucket acl check: action allowed: requestID(%v) userID(%v) accessKey(%v) volume(%v) action(%v)", GetRequestID(r), userInfo, param.AccessKey(), param.Bucket(), param.Action()) } } + +func (o *ObjectNode) loadBucketMeta(bucket string) (vol *Volume, acl *AccessControlPolicy, + policy *Policy, vv *proto.VolView, err error) { + if vol, err = o.getVol(bucket); err != nil { + return + } + if acl, err = vol.metaLoader.loadACL(); err != nil { + return + } + if policy, err = vol.metaLoader.loadPolicy(); err != nil { + return + } + if vv, err = o.mc.ClientAPI().GetVolumeWithoutAuthKey(bucket); err != nil { + return + } + return +} + +func (o *ObjectNode) allowedBySrcBucketPolicy(param *RequestParam, reqUid string) (err error) { + srcBucketId, srcKey, _, err := extractSrcBucketKey(param.r) + if err != nil { + log.LogDebugf("copySource(%v) argument invalid: requestID(%v)", param.r.Header.Get(HeaderNameXAmzCopySource), GetRequestID(param.r)) + return + } + vol, acl, policy, vv, err := o.loadBucketMeta(srcBucketId) + if err != nil { + log.LogErrorf("srcBucket policy check: load bucket metadata fail: requestID(%v) err(%v)", GetRequestID(param.r), err) + return + } + if vol != nil && policy != nil && !policy.IsEmpty() { + conditionCheck := map[string]string{ + SOURCEIP: param.sourceIP, + KEYNAME: srcKey, + REFERER: param.r.Referer(), + HOST: param.r.Host, + } + pcr := policy.IsAllowed(param, reqUid, vv.Owner, conditionCheck) + switch pcr { + case POLICY_ALLOW: + log.LogDebugf("srcBucket policy check: policy allowed: requestID(%v)", GetRequestID(param.r)) + return + case POLICY_DENY: + log.LogWarnf("srcBucket policy check: policy not allowed: requestID(%v) ", GetRequestID(param.r)) + return AccessDenied + case POLICY_UNKNOW: + // policy check result is unknown so that acl should be checked + log.LogWarnf("srcBucket policy check: policy unknown: requestID(%v) ", GetRequestID(param.r)) + } + } + + isOwner := reqUid == vv.Owner + if acl == nil && !isOwner { + log.LogWarnf("srcBucket acl check: not allowed because of empty bucket ACL : requestID(%v) ", GetRequestID(param.r)) + return AccessDenied + } + + if vol != nil && acl != nil && !acl.IsAclEmpty() { + allowed := acl.IsAllowed(param, isOwner) + if !allowed { + log.LogWarnf("srcBucket acl check: bucket ACL not allowed: requestID(%v) acl(%v) volume(%v) action(%v)", + GetRequestID(param.r), acl, param.Bucket(), param.Action()) + return + } + } + log.LogDebugf("srcBucket acl check: action allowed: requestID(%v) accessKey(%v) volume(%v) action(%v)", + GetRequestID(param.r), param.AccessKey(), param.Bucket(), param.Action()) + return +} diff --git a/objectnode/policy_action.go b/objectnode/policy_action.go index aa768fd0e..635b291cd 100644 --- a/objectnode/policy_action.go +++ b/objectnode/policy_action.go @@ -21,8 +21,8 @@ import ( "strings" "github.com/cubefs/cubefs/proto" - "github.com/cubefs/cubefs/util" + "github.com/google/uuid" ) @@ -49,27 +49,6 @@ func ActionFromRouteName(name string) proto.Action { return proto.ParseAction(name[:len(name)-33]) } -func (s Statement) checkActions(p *RequestParam) bool { - if s.Actions.Empty() { - return true - } - if s.Actions.ContainsWithAny(p.Action().String()) { - return true - } - return false -} - -func (s Statement) checkNotActions(p *RequestParam) bool { - if s.NotActions.Empty() { - return true - } - if s.NotActions.ContainsWithAny(p.Action().String()) { - return false - } - return true -} - -// func IsIntersectionActions(actions proto.Actions, action proto.Action) bool { if len(actions) == 0 { return true diff --git a/objectnode/policy_condition.go b/objectnode/policy_condition.go index b5938c08a..143b6a06b 100644 --- a/objectnode/policy_condition.go +++ b/objectnode/policy_condition.go @@ -20,416 +20,266 @@ package objectnode // https://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html import ( + "encoding/json" "fmt" - "net/http" - "strconv" - "strings" - "time" + "sort" + + "github.com/cubefs/cubefs/util/log" ) -//https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/example-bucket-policies.html -type ConditionValues map[string]StringSet -type Condition map[ConditionType]ConditionValues -type ConditionType string -type ConditionKey string +var ( + invalidCIDR = "value %v must be CIDR string for %v condition" + invalidIPKey = "only %v key is allowed for %v condition" + invalidStringValue = "value must be a string for %v condition" + invalidConditionKey = "invalid condition key '%v'" + emptyCondition = "condition must not be empty" + cannotHandledCondition = "condition %v cannot be handled" + cannotTransformToString = "%v cannot transform to string" + cannotHandledConditionValue = "cannot handle condition values: %v" + invalidConditionOperator = "invalid condition operator '%v'" + invalidConditionValue = "invalid value" + duplicateConditionValue = "duplicate value found '%v'" + unknownJsonData = "unknown json data '%v'" +) + +type operator string -// https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html const ( - IpAddress ConditionType = "IpAddress" - NotIpAddress = "NotIpAddress" - StringLike = "StringLike" - StringNotLike = "StringNotLike" - StringEquals = "StringEquals" - StringNotEquals = "StringNotEquals" - Bool = "Bool" - DateEquals = "DateEquals" - DateNotEquals = "DateNotEquals" - DateLessThan = "DateLessThan" - DateLessThanEquals = "DateLessThanEquals" - DateGreaterThan = "DateGreaterThan" - DateGreaterThanEquals = "DateGreaterThanEquals" - NumericEquals = "NumericEquals" - NumericNotEquals = "NumericNotEquals" - NumericLessThan = "NumericLessThan" - NumericLessThanEquals = "NumericLessThanEquals" - NumericGreaterThan = "NumericGreaterThan" - NumericGreaterThanEquals = "NumericGreaterThanEquals" - ArnEquals = "ArnEquals" - ArnLike = "ArnLike" // - ArnNotEquals = "ArnNotEquals" - ArnNotLike = "ArnNotLike" // + stringLike = "StringLike" + stringNotLike = "StringNotLike" + ipAddress = "IpAddress" + notIPAddress = "NotIpAddress" ) -var ( - StringFuncs = []ConditionFunc{StringEqualsFunc, StringNotEqualsFunc, StringLikeFunc, StringNotLikeFunc} - DateFuncs = []ConditionFunc{DateEqualsFunc, DateNotEqualsFunc, DateLessThanFunc, DateGreaterThanFunc, DateLessThanEqualsFunc, DateGreaterThanEqualsFunc} - IpAddressFuncs = []ConditionFunc{IpAddressFunc, NotIpAddressFunc} - BoolFuncs = []ConditionFunc{BoolFunc} - NumericFuncs = []ConditionFunc{NumericEqualsFunc, NumericNotEqualsFunc, NumericLessThanFunc, NumericLessThanEqualsFunc, NumericGreaterThanFunc, NumericGreaterThanEqualsFunc} - ArnFuncs = []ConditionFunc{ArnEqualsFunc, ArnNotEqualsFunc, ArnLikeFunc, ArnNotLikeFunc} -) - -type ConditionTypeSet map[ConditionType]null - -var ( - StringType = ConditionTypeSet{StringEquals: void, StringNotEquals: void, StringLike: void, StringNotLike: void} - DateType = ConditionTypeSet{DateEquals: void, DateNotEquals: void, DateLessThan: void, DateGreaterThan: void, DateLessThanEquals: void, DateGreaterThanEquals: void} - IpAddressType = ConditionTypeSet{IpAddress: void, NotIpAddress: void} - BoolType = ConditionTypeSet{Bool: void} - NumericType = ConditionTypeSet{NumericEquals: void, NumericNotEquals: void, NumericLessThan: void, NumericLessThanEquals: void, NumericGreaterThan: void, NumericGreaterThanEquals: void} - ArnType = ConditionTypeSet{ArnEquals: void, ArnNotEquals: void, ArnLike: void, ArnNotLike: void} -) - -// https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/reference_policies_condition-keys.html -const ( - AwsCurrentTime ConditionKey = "aws:CurrentTime" - AwsEpochTime = "aws:EpochTime" - AwsMultiFactorAuthPresent = "aws:MultiFactorAuthPresent" - AwsPrincipalAccount = "aws:PrincipalAccount" - AwsPrincipalArn = "aws:PrincipalArn" - AwsPrincipalOrgID = "aws:PrincipalOrgID" - AwsPrincipalTag = "aws:PrincipalTag" - AwsPrincipalType = "aws:PrincipalType" - AwsReferer = "aws:Referer" - AwsRequestRegion = "aws:RequestRegion" - AwsRequestTagKey = "aws:RequestTag/tag-key" - AwsResourceTagKey = "aws:ResourceTag/tag-key" - AwsSecureTransport = "aws:SecureTransport" - AwsSourceAccout = "aws:AwsSourceAccout" - AwsSourceArn = "aws:SourceArn" - AwsSourceIp = "aws:SourceIp" - AwsSourceVpc = "aws:SourceVpc" - AwsSourceVpce = "aws:SourceVpce" - AwsTagKeys = "aws:TagKeys" - AwsTokenIssueTime = "aws:TokenIssueTime" - AwsUserAgent = "aws:UserAgent" - AwsUserId = "aws:userid" - AwsUserName = "aws:username" - AwsVpcSourceIp = "aws:VpcSourceIp" -) - -var ConditionKeyType = map[ConditionKey]ConditionTypeSet{ - AwsCurrentTime: DateType, - AwsEpochTime: DateType, - AwsMultiFactorAuthPresent: BoolType, - AwsPrincipalAccount: StringType, - AwsPrincipalArn: ArnType, - AwsPrincipalOrgID: StringType, - AwsPrincipalTag: StringType, - AwsPrincipalType: StringType, - AwsReferer: StringType, - AwsRequestRegion: StringType, - AwsRequestTagKey: StringType, - AwsResourceTagKey: StringType, - AwsSecureTransport: BoolType, - AwsSourceAccout: StringType, - AwsSourceArn: ArnType, - AwsSourceIp: IpAddressType, - AwsSourceVpc: StringType, - AwsSourceVpce: StringType, - AwsTagKeys: StringType, - AwsTokenIssueTime: DateType, - AwsUserAgent: StringType, - AwsUserId: StringType, - AwsUserName: StringType, - AwsVpcSourceIp: IpAddressType, +var supportedOperators = []operator{ + stringLike, + stringNotLike, + ipAddress, + notIPAddress, + // Add new conditions here. } -var ConditionFuncMap = map[ConditionType]ConditionFunc{ - IpAddress: IpAddressFunc, - NotIpAddress: NotIpAddressFunc, - StringLike: StringLikeFunc, - StringNotLike: StringNotLikeFunc, - StringEquals: StringEqualsFunc, - StringNotEquals: StringNotEqualsFunc, - Bool: BoolFunc, - DateEquals: DateEqualsFunc, - DateNotEquals: DateNotEqualsFunc, - DateLessThan: DateLessThanFunc, - DateLessThanEquals: DateLessThanEqualsFunc, - DateGreaterThan: DateGreaterThanFunc, - DateGreaterThanEquals: DateGreaterThanEqualsFunc, -} - -type ConditionFunc func(p *RequestParam, values ConditionValues) bool - -var ( - awsTrimedPrefix = []string{"aws:", "jwt:", "s3:"} -) - -func TrimAwsPrefixKey(key string) string { - for _, prefix := range awsTrimedPrefix { - if strings.HasPrefix(key, prefix) { - return strings.TrimPrefix(key, prefix) - } - } - - return key -} - -func getCondtionValues(r *http.Request) map[string][]string { - currentTime := time.Now().UTC() - authInfo := parseRequestAuthInfo(r) - accessKey := authInfo.accessKey - principalType := "User" - if accessKey == "" { - principalType = "Anonymous" - } - values := map[string][]string{ - "SourceIp": {getRequestIP(r)}, - "UserAgent": {r.UserAgent()}, - "Referer": {r.Referer()}, - "CurrentTime": {currentTime.Format(AMZTimeFormat)}, - "EpochTime": {fmt.Sprintf("%d", currentTime.Unix())}, - "userid": {accessKey}, - "username": {accessKey}, - "PrincipalType": {principalType}, - } - - for k, v := range r.Header { - if existsV, ok := values[k]; ok { - values[k] = append(existsV, v...) - } else { - values[k] = v - } - } - - for k, v := range r.URL.Query() { - if existsV, ok := values[k]; ok { - values[k] = append(existsV, v...) - } else { - values[k] = v - } - } - - return values -} - -func IpAddressFunc(p *RequestParam, value ConditionValues) bool { - key := TrimAwsPrefixKey(AwsSourceIp) - canonicalKey := http.CanonicalHeaderKey(key) - sourceIP, ok := value[canonicalKey] - if !ok { - return false - } - for ipnet := range sourceIP.values { - if ok, _ := isIPNetContainsIP(p.sourceIP, ipnet); ok { +func (op operator) IsValid() bool { + for _, supOp := range supportedOperators { + if op == supOp { return true } } - return true -} - -func NotIpAddressFunc(p *RequestParam, values ConditionValues) bool { - return !IpAddressFunc(p, values) -} - -func StringLikeFunc(reqParam *RequestParam, storeCondVals ConditionValues) bool { - for k, storeVals := range storeCondVals { - key := TrimAwsPrefixKey(k) - canonicalKey := http.CanonicalHeaderKey(key) - if reqVals, ok := reqParam.conditionVars[canonicalKey]; ok { - for _, rv := range reqVals { - for sv := range storeVals.values { - if match := patternMatch(rv, sv); match { - return true - } - } - } - } - } - return false } -func StringNotLikeFunc(p *RequestParam, values ConditionValues) bool { - return !StringLikeFunc(p, values) -} - -func StringEqualsFunc(reqParam *RequestParam, storeCondVals ConditionValues) bool { - for k, storeVals := range storeCondVals { - key := TrimAwsPrefixKey(k) - canonicalKey := http.CanonicalHeaderKey(key) - if reqVals, ok := reqParam.conditionVars[canonicalKey]; ok { - for _, rv := range reqVals { - if storeVals.Contains(rv) { - return true - } - } - } else if reqVals, ok := reqParam.conditionVars[key]; ok { - for _, rv := range reqVals { - if storeVals.Contains(rv) { - return true - } - } - } +// encodes operator to JSON data. +func (op operator) MarshalJSON() ([]byte, error) { + if !op.IsValid() { + return nil, fmt.Errorf("invalid operator %v", op) } - return false + return json.Marshal(string(op)) } -func StringNotEqualsFunc(p *RequestParam, values ConditionValues) bool { - return !StringNotEqualsFunc(p, values) -} - -// check statement conditions -func (s Statement) checkConditions(param *RequestParam) bool { - if len(s.Condition) == 0 { - return true +// decodes JSON data to condition operator. +func (op *operator) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err } - for k, v := range s.Condition { - f, ok := ConditionFuncMap[k] - if !ok { - continue - } - if !f(param, v) { + + parsedOperator, err := parseOperator(s) + if err != nil { + return err + } + + *op = parsedOperator + return nil +} + +func parseOperator(s string) (operator, error) { + n := operator(s) + + if n.IsValid() { + return n, nil + } + + return n, fmt.Errorf(invalidConditionOperator, s) +} + +type Operation interface { + // evaluates this condition operation with given values. + evaluate(values map[string]string) bool + + // returns all condition keys used in this operation. + keys() KeySet + + // returns condition operator of this operation. + operator() operator + + toMap() map[Key]ValueSet +} + +type Condition []Operation + +// evaluates all operation with given values map. +func (operations Condition) Evaluate(values map[string]string) bool { + for _, op := range operations { + if !op.evaluate(values) { + log.LogDebugf("cannot match a condition, operator:%v, conditionKV:%v, values: %v", op.operator(), op.toMap(), values) return false } } - return true } -func BoolFunc(p *RequestParam, policyCondtion ConditionValues) bool { - if len(policyCondtion) == 0 { - return true +// returns list of keys used in all operation. +func (operations Condition) Keys() KeySet { + keySet := NewKeySet() + + for _, op := range operations { + keySet.AddAll(op.keys()) } - for condKey, condVal := range policyCondtion { - for vals := range condVal.values { - val1, _ := strconv.ParseBool(vals) - if cond, ok := p.conditionVars[condKey]; ok { - for _, c := range cond { - val2, _ := strconv.ParseBool(c) - return val1 == val2 - } + + return keySet +} + +// encodes Condition to JSON data. +func (operations Condition) MarshalJSON() ([]byte, error) { + conditionKV := make(map[operator]map[Key]ValueSet) + + for _, op := range operations { + if _, ok := conditionKV[op.operator()]; ok { + for k, v := range op.toMap() { + conditionKV[op.operator()][k] = v } + } else { + conditionKV[op.operator()] = op.toMap() } } - return false + return json.Marshal(conditionKV) } -func DateEqualsFunc(p *RequestParam, policyVals ConditionValues) bool { - for k, pVals := range policyVals { - for pVal := range pVals.values { - pDate, err := time.Parse(AMZTimeFormat, pVal) +func (operations Condition) String() string { + var opStrings []string + for _, op := range operations { + s := fmt.Sprintf("%v", op) + opStrings = append(opStrings, s) + } + sort.Strings(opStrings) + + return fmt.Sprintf("%v", opStrings) +} + +var conditionOpMap = map[operator]func(map[Key]ValueSet) (Operation, error){ + stringLike: newStringLikeOp, + stringNotLike: newStringNotLikeOp, + ipAddress: newIPAddressOp, + notIPAddress: newNotIPAddressOp, + // Add new conditions here. +} + +// decodes JSON data to Condition. +func (operations *Condition) UnmarshalJSON(data []byte) error { + operatorMap := make(map[string]map[string]ValueSet) + if err := json.Unmarshal(data, &operatorMap); err != nil { + return err + } + + if len(operatorMap) == 0 { + return fmt.Errorf(emptyCondition) + } + + var ops []Operation + for operatorString, args := range operatorMap { + o, err := parseOperator(operatorString) + if err != nil { + return err + } + + vfn, ok := conditionOpMap[o] + if !ok { + return fmt.Errorf(cannotHandledCondition, o) + } + m := make(map[Key]ValueSet) + for keyString, values := range args { + key, err := parseKey(keyString) if err != nil { - return false + return err } - if reqVals, ok := p.conditionVars[k]; ok { - for _, reqVal := range reqVals { - reqDate, err := time.Parse(AMZTimeFormat, reqVal) - if err != nil { - return false - } - return pDate.Equal(reqDate) - } + m[key] = values + + } + op, err := vfn(m) + if err != nil { + return err + } + ops = append(ops, op) + } + + *operations = ops + + return nil +} + +func (operations Condition) CheckValid() error { + for _, op := range operations { + if !op.operator().IsValid() { + return NewError("InvalidConditionType", fmt.Sprintf("policy has invalid condition type: %s", op.operator()), 400) + } + for key := range op.keys() { + if !key.IsValid() { + return NewError("InvalidConditionKey", fmt.Sprintf("policy has invalid condition key: %s", key), 400) } } } - - return false + return nil } -func DateNotEqualsFunc(p *RequestParam, value ConditionValues) bool { - return !DateEqualsFunc(p, value) -} +// parse a map into Condition +func (operations *Condition) parseOperations(operatorMap map[string]map[string]interface{}) error { + var ops []Operation + for operatorString, args := range operatorMap { + o, err := parseOperator(operatorString) + if err != nil { + return err + } -func DateLessThanFunc(p *RequestParam, value ConditionValues) bool { - for k, pVals := range value { - for pVal := range pVals.values { - pDate, err := time.Parse(AMZTimeFormat, pVal) + vfn, ok := conditionOpMap[o] + if !ok { + return fmt.Errorf(cannotHandledCondition, o) + } + m := make(map[Key]ValueSet) + for keyString, values := range args { + + valueSet := NewValueSet() + key, err := parseKey(keyString) if err != nil { - return false + return err } - if reqVals, ok := p.conditionVars[k]; ok { - for _, reqVal := range reqVals { - reqDate, err := time.Parse(AMZTimeFormat, reqVal) - if err != nil { - return false + switch values.(type) { + case string: + valueSet.Add(NewStringValue(values.(string))) + case []interface{}: + for _, value := range values.([]interface{}) { + if valueString, ok := value.(string); ok { + valueSet.Add(NewStringValue(valueString)) + } else { + return fmt.Errorf(cannotTransformToString, value) } - return reqDate.Before(pDate) } + default: + + return fmt.Errorf(cannotHandledConditionValue, values) } + m[key] = valueSet + } - } - return false -} - -func DateLessThanEqualsFunc(p *RequestParam, value ConditionValues) bool { - for k, pVals := range value { - for pVal := range pVals.values { - pDate, err := time.Parse(AMZTimeFormat, pVal) - if err != nil { - return false - } - if reqVals, ok := p.conditionVars[k]; ok { - for _, reqVal := range reqVals { - reqDate, err := time.Parse(AMZTimeFormat, reqVal) - if err != nil { - return false - } - return reqDate.Before(pDate) || reqDate.Equal(pDate) - } - } + op, err := vfn(m) + if err != nil { + return err } + ops = append(ops, op) } - return true -} - -func DateGreaterThanFunc(p *RequestParam, value ConditionValues) bool { - return !DateLessThanEqualsFunc(p, value) -} - -func DateGreaterThanEqualsFunc(p *RequestParam, value ConditionValues) bool { - return !DateLessThanFunc(p, value) -} - -func NumericEqualsFunc(p *RequestParam, value ConditionValues) bool { - //TODO: numeric equal implements - - return true -} - -func NumericNotEqualsFunc(p *RequestParam, value ConditionValues) bool { - return !NumericEqualsFunc(p, value) -} - -func NumericLessThanFunc(p *RequestParam, value ConditionValues) bool { - //TODO: numeric less than implements - - return true -} - -func NumericLessThanEqualsFunc(p *RequestParam, value ConditionValues) bool { - return NumericLessThanFunc(p, value) || NumericEqualsFunc(p, value) -} - -func NumericGreaterThanFunc(p *RequestParam, value ConditionValues) bool { - return !NumericLessThanEqualsFunc(p, value) -} - -func NumericGreaterThanEqualsFunc(p *RequestParam, value ConditionValues) bool { - return !NumericLessThanFunc(p, value) -} - -func ArnEqualsFunc(p *RequestParam, value ConditionValues) bool { - //TODO: numeric equal implements - - return true -} - -func ArnNotEqualsFunc(p *RequestParam, value ConditionValues) bool { - return !ArnEqualsFunc(p, value) -} - -func ArnLikeFunc(p *RequestParam, value ConditionValues) bool { - //TODO: numeric equal implements - - return true -} - -func ArnNotLikeFunc(p *RequestParam, value ConditionValues) bool { - return !ArnLikeFunc(p, value) + + *operations = ops + return nil } diff --git a/objectnode/policy_condition_test.go b/objectnode/policy_condition_test.go new file mode 100644 index 000000000..7bc4c3861 --- /dev/null +++ b/objectnode/policy_condition_test.go @@ -0,0 +1,261 @@ +package objectnode + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCondition_UnmarshalJSON(t *testing.T) { + + op1, err := newIPAddressOp(map[Key]ValueSet{AWSSourceIP: NewValueSet(NewStringValue("1.1.1.1"))}) + require.NoError(t, err) + var expectCondition1 Condition = []Operation{op1} + op2, err := newStringLikeOp(map[Key]ValueSet{AWSReferer: NewValueSet(NewStringValue("*.abc.com"))}) + require.NoError(t, err) + var expectCondition2 Condition = []Operation{op2} + op3, err := newStringLikeOp(map[Key]ValueSet{AWSHost: NewValueSet(NewStringValue("*.cba.com"))}) + require.NoError(t, err) + var expectCondition3 Condition = []Operation{op3} + testCases := []struct { + Value string + expect Condition + expectErr error + }{ + {`{"IpAddress": {"aws:SourceIp": ["1.1.1.1"]}}`, expectCondition1, nil}, + {`{"StringLike": {"aws:Referer": ["*.abc.com"]}}`, expectCondition2, nil}, + {`{"StringLike": {"aws:Host": ["*.cba.com"]}}`, expectCondition3, nil}, + {`[]`, nil, errors.New(" cannot unmarshal array")}, + {`{}`, nil, errors.New("condition must not be empty")}, + {`{"stringlike": {"aws:Host": ["*.cba.com"]}}`, nil, errors.New("invalid condition operator")}, + {`{"StringLike": {"aws:host": ["*.cba.com"]}}`, nil, errors.New("invalid condition key")}, + {`{"StringLike": {"aws:Host": [123]}}`, nil, errors.New("value must be a string")}, + } + + for i, testCase := range testCases { + var result Condition + resultErr := json.Unmarshal([]byte(testCase.Value), &result) + if testCase.expectErr != nil { + require.Error(t, resultErr, fmt.Sprintf("test case %v", i+1)) + require.Contains(t, resultErr.Error(), testCase.expectErr.Error(), fmt.Sprintf("test case %v", i+1)) + } else { + t.Log(result) + require.NoError(t, resultErr, fmt.Sprintf("test case %v", i+1)) + require.Equal(t, testCase.expect, result, fmt.Sprintf("test case %v", i+1)) + } + } + +} + +func TestCondition_CheckValid(t *testing.T) { + op1, err := newIPAddressOp(map[Key]ValueSet{AWSSourceIP: NewValueSet(NewStringValue("1.1.1.1"))}) + require.NoError(t, err) + var Condition1 Condition = []Operation{op1} + op2, err := newStringLikeOp(map[Key]ValueSet{AWSReferer: NewValueSet(NewStringValue("*.abc.com"))}) + require.NoError(t, err) + var Condition2 Condition = []Operation{op2} + op3, err := newStringLikeOp(map[Key]ValueSet{AWSHost: NewValueSet(NewStringValue("*.cba.com"))}) + require.NoError(t, err) + var Condition3 Condition = []Operation{op3} + op4, err := newStringLikeOp(map[Key]ValueSet{"host": NewValueSet(NewStringValue("*.cba.com"))}) + require.NoError(t, err) + var Condition4 Condition = []Operation{op4} + + testCases := []struct { + Value Condition + expectedErr error + }{ + {Condition1, nil}, + {Condition2, nil}, + {Condition3, nil}, + {Condition4, errors.New("policy has invalid condition key")}, + } + + for i, testCase := range testCases { + + resultErr := testCase.Value.CheckValid() + if testCase.expectedErr != nil { + require.Error(t, resultErr, fmt.Sprintf("test case %v", i+1)) + require.Contains(t, resultErr.Error(), testCase.expectedErr.Error(), fmt.Sprintf("test case %v", i+1)) + } else { + require.NoError(t, resultErr, fmt.Sprintf("test case %v", i+1)) + } + } +} + +func TestCondition_MarshalJSON(t *testing.T) { + op1, err := newIPAddressOp(map[Key]ValueSet{AWSSourceIP: NewValueSet(NewStringValue("1.1.1.1"))}) + require.NoError(t, err) + var Condition1 Condition = []Operation{op1} + op2, err := newStringLikeOp(map[Key]ValueSet{AWSReferer: NewValueSet(NewStringValue("*.abc.com"))}) + require.NoError(t, err) + var Condition2 Condition = []Operation{op2} + op3, err := newStringLikeOp(map[Key]ValueSet{AWSHost: NewValueSet(NewStringValue("*.cba.com"))}) + require.NoError(t, err) + var Condition3 Condition = []Operation{op3, op2} + + op4, err := newNotIPAddressOp(map[Key]ValueSet{AWSSourceIP: NewValueSet(NewStringValue("1.1.1.1"))}) + require.NoError(t, err) + var Condition4 Condition = []Operation{op4} + op5, err := newStringNotLikeOp(map[Key]ValueSet{AWSReferer: NewValueSet(NewStringValue("*.abc.com"))}) + require.NoError(t, err) + var Condition5 Condition = []Operation{op5} + op6, err := newStringNotLikeOp(map[Key]ValueSet{AWSHost: NewValueSet(NewStringValue("*.cba.com"))}) + require.NoError(t, err) + var Condition6 Condition = []Operation{op6} + testCases := []struct { + Expect string + Value Condition + expectErr error + }{ + {`{"IpAddress":{"aws:SourceIp":["1.1.1.1/32"]}}`, Condition1, nil}, + {`{"StringLike":{"aws:Referer":["*.abc.com"]}}`, Condition2, nil}, + {`{"StringLike":{"aws:Host":["*.cba.com"],"aws:Referer":["*.abc.com"]}}`, Condition3, nil}, + {`{"NotIpAddress":{"aws:SourceIp":["1.1.1.1/32"]}}`, Condition4, nil}, + {`{"StringNotLike":{"aws:Referer":["*.abc.com"]}}`, Condition5, nil}, + {`{"StringNotLike":{"aws:Host":["*.cba.com"]}}`, Condition6, nil}, + } + + for i, testCase := range testCases { + + b, resultErr := json.Marshal(testCase.Value) + if testCase.expectErr != nil { + require.Error(t, resultErr, fmt.Sprintf("test case %v", i+1)) + require.Contains(t, resultErr.Error(), testCase.expectErr.Error(), fmt.Sprintf("test case %v", i+1)) + } else { + require.NoError(t, resultErr, fmt.Sprintf("test case %v", i+1)) + require.Equal(t, testCase.Expect, string(b), fmt.Sprintf("test case %v", i+1)) + } + } +} + +func TestCondition_Evaluate(t *testing.T) { + op1, err := newIPAddressOp(map[Key]ValueSet{AWSSourceIP: NewValueSet(NewStringValue("1.1.1.1"))}) + require.NoError(t, err) + var Condition1 Condition = []Operation{op1} + op2, err := newStringLikeOp(map[Key]ValueSet{AWSReferer: NewValueSet(NewStringValue("*.abc.com"))}) + require.NoError(t, err) + var Condition2 Condition = []Operation{op2} + op3, err := newStringLikeOp(map[Key]ValueSet{AWSHost: NewValueSet(NewStringValue("*.cba.com"))}) + require.NoError(t, err) + var Condition3 Condition = []Operation{op3, op2} + + op4, err := newNotIPAddressOp(map[Key]ValueSet{AWSSourceIP: NewValueSet(NewStringValue("1.1.1.1"))}) + require.NoError(t, err) + var Condition4 Condition = []Operation{op4} + op5, err := newStringNotLikeOp(map[Key]ValueSet{AWSReferer: NewValueSet(NewStringValue("*.abc.com"))}) + require.NoError(t, err) + var Condition5 Condition = []Operation{op5} + op6, err := newStringNotLikeOp(map[Key]ValueSet{AWSHost: NewValueSet(NewStringValue("*.cba.com"))}) + require.NoError(t, err) + var Condition6 Condition = []Operation{op6} + + testCases := []struct { + Values map[string]string + Cond Condition + expect bool + }{ + {map[string]string{"SourceIp": "1.1.1.1"}, Condition1, true}, + {map[string]string{"Referer": "www.abc.com"}, Condition2, true}, + {map[string]string{"Host": "www.cba.com", "Referer": "www.abc.com"}, Condition3, true}, + + {map[string]string{"SourceIp": "1.1.1.2"}, Condition1, false}, + {map[string]string{"Referer": "www.abcd.com"}, Condition2, false}, + {map[string]string{"Host": "www.dcba.com", "Referer": "www.abc.com"}, Condition3, false}, + {map[string]string{"Host": "www.cba.com", "Referer": "www.abcd.com"}, Condition3, false}, + + {map[string]string{"SourceIp": "1.1.1.1"}, Condition4, false}, + {map[string]string{"Referer": "www.abc.com"}, Condition5, false}, + {map[string]string{"Host": "www.cba.com"}, Condition6, false}, + + {map[string]string{"SourceIp": "1.1.1.2"}, Condition4, true}, + {map[string]string{"Referer": "www.abcd.com"}, Condition5, true}, + {map[string]string{"Host": "www.dcba.com"}, Condition6, true}, + } + + for i, testCase := range testCases { + + b := testCase.Cond.Evaluate(testCase.Values) + require.Equal(t, testCase.expect, b, fmt.Sprintf("test case %v", i+1)) + + } +} + +func TestNameIsValid(t *testing.T) { + testCases := []struct { + operator operator + expectedResult bool + }{ + {stringLike, true}, + {stringNotLike, true}, + {ipAddress, true}, + {notIPAddress, true}, + } + + for i, testCase := range testCases { + result := testCase.operator.IsValid() + + if testCase.expectedResult != result { + t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result) + } + } +} + +func TestNameMarshalJSON(t *testing.T) { + testCases := []struct { + operator operator + expectedResult []byte + expectErr bool + }{ + {stringLike, []byte(`"StringLike"`), false}, + {stringNotLike, []byte(`"StringNotLike"`), false}, + {ipAddress, []byte(`"IpAddress"`), false}, + {notIPAddress, []byte(`"NotIpAddress"`), false}, + } + + for i, testCase := range testCases { + result, err := json.Marshal(testCase.operator) + expectErr := err != nil + + if testCase.expectErr != expectErr { + t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr) + } + + if !testCase.expectErr { + if !reflect.DeepEqual(result, testCase.expectedResult) { + t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result)) + } + } + } +} + +func TestNameUnmarshalJSON(t *testing.T) { + testCases := []struct { + data []byte + expectedResult operator + expectErr bool + }{ + {[]byte(`"StringLike"`), stringLike, false}, + {[]byte(`"foo"`), operator(""), true}, + } + + for i, testCase := range testCases { + var result operator + err := json.Unmarshal(testCase.data, &result) + expectErr := (err != nil) + + if testCase.expectErr != expectErr { + t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr) + } + + if !testCase.expectErr { + if testCase.expectedResult != result { + t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result) + } + } + } +} diff --git a/objectnode/policy_const.go b/objectnode/policy_const.go new file mode 100644 index 000000000..3dbcb50a9 --- /dev/null +++ b/objectnode/policy_const.go @@ -0,0 +1,136 @@ +// Copyright 2023 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 + +type PolicyCheckResult int + +const ( + POLICY_UNKNOW PolicyCheckResult = iota + 1 //no policy or not match + POLICY_ALLOW + POLICY_DENY +) + +type ConditionEnum int + +const ( + KEYNAME = "KeyName" + SOURCEIP = "SourceIp" + REFERER = "Referer" + HOST = "Host" +) +const ( + Principal_Any PrincipalElementType = "*" + S3_ACTION_PREFIX = "s3:" + S3_RESOURCE_PREFIX = "arn:aws:s3:::" + S3_PRINCIPAL_PREFIX = "AWS" +) + +//if more s3 api is supported by policy, need extend bucketApiList, objectApiList +var bucketApiList = SliceString{LIST_OBJECTS, LIST_OBJECTS_V2, HEAD_BUCKET, DELETE_BUCKET, LIST_MULTIPART_UPLOADS, GET_BUCKET_LOCATION, GET_OBJECT_LOCK_CFG, PUT_OBJECT_LOCK_CFG} +var objectApiList = SliceString{GET_OBJECT, HEAD_OBJECT, DELETE_OBJECT, PUT_OBJECT, POST_OBJECT, INITIALE_MULTIPART_UPLOAD, UPLOAD_PART, UPLOAD_PART_COPY, COMPLETE_MULTIPART_UPLOAD, COPY_OBJECT, ABORT_MULTIPART_UPLOAD, LIST_PARTS, BATCH_DELETE, GET_OBJECT_RETENTION} + +type SliceString []string + +//https://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html +const ( + //object level + ACTION_GET_OBJECT = "getobject" + ACTION_DELETE_OBJECT = "deleteobject" + ACTION_PUT_OBJECT = "putobject" + ACTION_ABORT_MULTIPART_UPLOAD = "abortmultipartupload" + ACTION_LIST_MULTIPART_UPLOAD_PARTS = "listmultipartuploadparts" + ACTION_GET_OBJECT_RETENTION = "getobjectretention" + + //bucket level + ACTION_LIST_BUCKET = "listbucket" + ACTION_DELETE_BUCKET = "deletebucket" + ACTION_LIST_BUCKET_MULTIPART_UPLOADS = "listbucketmultipartuploads" + ACTION_GET_BUCKET_LOCATION = "getbucketlocation" + ACTION_GET_OBJECT_LOCK_CFG = "getobjectlockconfiguration" + ACTION_PUT_OBJECT_LOCK_CFG = "putobjectlockconfiguration" + + //bucket + object level + ACTION_ANY = "*" +) + +// action => api list, this should be consistent with bucketApiList&&objectApiList +var S3ActionToApis = map[string]SliceString{ + ACTION_PUT_OBJECT: {PUT_OBJECT, POST_OBJECT, COPY_OBJECT, INITIALE_MULTIPART_UPLOAD, UPLOAD_PART, UPLOAD_PART_COPY, COMPLETE_MULTIPART_UPLOAD}, + ACTION_GET_OBJECT: {GET_OBJECT, HEAD_OBJECT}, + ACTION_DELETE_OBJECT: {DELETE_OBJECT, BATCH_DELETE}, + ACTION_ABORT_MULTIPART_UPLOAD: {ABORT_MULTIPART_UPLOAD}, + ACTION_LIST_BUCKET: {LIST_OBJECTS, LIST_OBJECTS_V2, HEAD_BUCKET}, + ACTION_LIST_MULTIPART_UPLOAD_PARTS: {LIST_PARTS}, + ACTION_LIST_BUCKET_MULTIPART_UPLOADS: {LIST_MULTIPART_UPLOADS}, + ACTION_DELETE_BUCKET: {DELETE_BUCKET}, + ACTION_GET_BUCKET_LOCATION: {GET_BUCKET_LOCATION}, + ACTION_GET_OBJECT_LOCK_CFG: {GET_OBJECT_LOCK_CFG}, + ACTION_PUT_OBJECT_LOCK_CFG: {PUT_OBJECT_LOCK_CFG}, + ACTION_GET_OBJECT_RETENTION: {GET_OBJECT_RETENTION}, +} + +var allowAnonymousActions = SliceString{ACTION_GET_OBJECT} + +//if more bucket actions support policy, need extend validBucketActions +var validBucketActions = SliceString{ACTION_LIST_BUCKET, ACTION_DELETE_BUCKET, ACTION_LIST_BUCKET_MULTIPART_UPLOADS, ACTION_GET_BUCKET_LOCATION, ACTION_PUT_OBJECT_LOCK_CFG, ACTION_GET_OBJECT_LOCK_CFG} + +func isPolicyApi(apiName string) bool { + return apiName == PUT_BUCKET_POLICY || apiName == GET_BUCKET_POLICY || apiName == DELETE_BUCKET_POLICY +} + +func (list SliceString) Contain(v string) bool { + for _, s := range list { + if s == v { + return true + } + } + return false +} + +func IsBucketApi(apiName string) bool { + return bucketApiList.Contain(apiName) +} + +func (p PolicyCheckResult) String() string { + switch p { + case POLICY_ALLOW: + return "allow" + case POLICY_DENY: + return "deny" + default: + return "unknow" + } +} + +func supportByPolicy(apiName string) bool { + return bucketApiList.Contain(apiName) || objectApiList.Contain(apiName) +} + +func IsAccountLevelApi(apiName string) bool { + return apiName == PUT_BUCKET || apiName == List_BUCKETS +} + +func isAnonymous(accessKey string) bool { + return accessKey == "" +} + +func apiAllowAnonymous(apiName string) bool { + for _, action := range allowAnonymousActions { + if S3ActionToApis[action].Contain(apiName) { + return true + } + } + return false +} diff --git a/objectnode/policy_ipaddressop.go b/objectnode/policy_ipaddressop.go new file mode 100644 index 000000000..11664a61e --- /dev/null +++ b/objectnode/policy_ipaddressop.go @@ -0,0 +1,184 @@ +// Copyright 2023 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 ( + "fmt" + "net" + "net/http" + "strconv" + "strings" +) + +// IP address operation. It checks whether value by Key in given +// values is in IP network. Here Key must be AWSSourceIP. +type ipAddressOp struct { + m map[Key][]*IPInfo +} +type IPInfo struct { + IP net.IP + Net *net.IPNet +} + +// evaluates to check whether IP address in values map for AWSSourceIP +// falls in one of network or not. +func (op ipAddressOp) evaluate(values map[string]string) bool { + for k, v := range op.m { + + requestValue, ok := values[http.CanonicalHeaderKey(k.Name())] + if !ok { + requestValue = values[k.Name()] + } + IP := net.ParseIP(requestValue) + if IP == nil { + panic(fmt.Errorf("invalid IP address '%v'", requestValue)) + } + nothingMatched := true //all values not matched + for _, IPInfo := range v { + if IPInfo.Net.Contains(IP) { + nothingMatched = false + } + } + if nothingMatched { + return false + } + } + return true +} + +// returns condition key which is used by this condition operation. +// Key is always AWSSourceIP. +func (op ipAddressOp) keys() KeySet { + + keys := make(KeySet) + for key := range op.m { + keys.Add(key) + } + return keys +} + +// operator() - returns "IpAddress" condition operator. +func (op ipAddressOp) operator() operator { + return ipAddress +} + +// toMap - returns map representation of this operation. +func (op ipAddressOp) toMap() map[Key]ValueSet { + resultMap := make(map[Key]ValueSet) + for k, v := range op.m { + if !k.IsValid() { + return nil + } + + values := NewValueSet() + for _, value := range v { + leadingOne, _ := value.Net.Mask.Size() + ip := value.IP.String() + "/" + strconv.Itoa(leadingOne) + values.Add(NewStringValue(ip)) + } + resultMap[k] = values + } + return resultMap +} + +// Not IP address operation. It checks whether value by Key in given +// values is NOT in IP network. Here Key must be AWSSourceIP. + +type notIPAddressOp struct { + ipAddressOp +} + +// evaluates to check whether IP address in values map for AWSSourceIP +// does not fall in one of network. +func (op notIPAddressOp) evaluate(values map[string]string) bool { + return !op.ipAddressOp.evaluate(values) +} + +// returns "NotIpAddress" condition operator. +func (op notIPAddressOp) operator() operator { + return notIPAddress +} + +func valuesToIPNets(op operator, values ValueSet) ([]*IPInfo, error) { + var IPInfos []*IPInfo + for v := range values { + s, err := v.GetString() + if err != nil { + return nil, fmt.Errorf(invalidCIDR, v, op) + } + + if strings.Index(s, "/") < 0 { + s += "/32" + } + IP, IPNet, err := net.ParseCIDR(s) + if err != nil { + return nil, fmt.Errorf(invalidCIDR, s, op) + } + IPInfo := &IPInfo{IP, IPNet} + IPInfos = append(IPInfos, IPInfo) + } + + return IPInfos, nil +} + +// returns new IP address operation. +func newIPAddressOp(m map[Key]ValueSet) (Operation, error) { + newMap := make(map[Key][]*IPInfo) + for k, v := range m { + IPNets, err := valuesToIPNets(ipAddress, v) + if err != nil { + return nil, err + } + newMap[k] = IPNets + } + + return NewIPAddressOp(newMap) +} + +//returns new IP address operation. +func NewIPAddressOp(m map[Key][]*IPInfo) (Operation, error) { + for key := range m { + if key != AWSSourceIP { + return nil, fmt.Errorf(invalidIPKey, AWSSourceIP, ipAddress) + } + } + + return &ipAddressOp{m: m}, nil +} + +//returns new Not IP address operation. +func newNotIPAddressOp(m map[Key]ValueSet) (Operation, error) { + newMap := make(map[Key][]*IPInfo) + for k, v := range m { + IPNets, err := valuesToIPNets(notIPAddress, v) + if err != nil { + return nil, err + } + newMap[k] = IPNets + } + + return NewNotIPAddressOp(newMap) +} + +// returns new Not IP address operation. +func NewNotIPAddressOp(m map[Key][]*IPInfo) (Operation, error) { + for key := range m { + if key != AWSSourceIP { + return nil, fmt.Errorf(invalidIPKey, AWSSourceIP, notIPAddress) + } + } + + return ¬IPAddressOp{ipAddressOp{m: m}}, nil +} diff --git a/objectnode/policy_ipaddressop_test.go b/objectnode/policy_ipaddressop_test.go new file mode 100644 index 000000000..25d40b1f2 --- /dev/null +++ b/objectnode/policy_ipaddressop_test.go @@ -0,0 +1,39 @@ +package objectnode + +import "testing" + +func TestIPAddressOpEvalute(t *testing.T) { + case1Operation, err := newIPAddressOp(map[Key]ValueSet{AWSSourceIP: NewValueSet(NewStringValue("192.168.1.0/24"))}) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + testCases := []struct { + operation Operation + values map[string]string + expectedResult bool + }{ + {case1Operation, map[string]string{"SourceIp": "192.168.1.10"}, true}, + {case1Operation, map[string]string{"SourceIp": "192.168.2.10"}, false}, + } + + for i, testCase := range testCases { + result := testCase.operation.evaluate(testCase.values) + + if result != testCase.expectedResult { + t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } +} + +func TestIPAddressOpKeys(t *testing.T) { + case1Operation, err := newIPAddressOp(map[Key]ValueSet{AWSSourceIP: NewValueSet(NewStringValue("192.168.1.0/24"))}) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + result := case1Operation.keys() + expect := NewKeySet(AWSSourceIP) + if len(result.Difference(expect)) != 0 || len(expect.Difference(result)) != 0 { + t.Fatalf("expect two value set equal, expected: %v, got: %v\n", expect, result) + } + +} diff --git a/objectnode/policy_keyset.go b/objectnode/policy_keyset.go new file mode 100644 index 000000000..8598aab86 --- /dev/null +++ b/objectnode/policy_keyset.go @@ -0,0 +1,175 @@ +// Copyright 2023 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 ( + "encoding/json" + "fmt" + "strings" +) + +// Key - conditional key which is used to fetch values for any condition. +// Refer https://docs.aws.amazon.com/IAM/latest/UserGuide/list_s3.html +// for more information about available condition keys. +type Key string + +const ( + // AWSReferer - key representing Referer header of any API. + AWSReferer Key = "aws:Referer" + + // AWSSourceIP - key representing client's IP address (not intermittent proxies) of any API. + AWSSourceIP Key = "aws:SourceIp" + + //AWSHost - key representing client's request host of any API, this is not standard AWS key + AWSHost Key = "aws:Host" +) + +var AllSupportedKeys = []Key{ + AWSReferer, + AWSSourceIP, + AWSHost, + // Add new supported condition keys. +} + +func (key Key) IsValid() bool { + for _, supKey := range AllSupportedKeys { + if supKey == key { + return true + } + } + + return false +} + +// encodes Key to JSON data. +func (key Key) MarshalJSON() ([]byte, error) { + if !key.IsValid() { + return nil, fmt.Errorf("unknown key: %v", key) + } + + return json.Marshal(string(key)) +} + +//returns key operator which is stripped value of prefixes "aws:" and "s3:" +func (key Key) Name() string { + keyString := string(key) + + if strings.HasPrefix(keyString, "aws:") { + return strings.TrimPrefix(keyString, "aws:") + } + + return strings.TrimPrefix(keyString, "s3:") +} + +// decodes string data to Key. +func (key *Key) UnmarshalText(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + parsedKey, err := parseKey(s) + if err != nil { + return err + } + + *key = parsedKey + return nil +} + +// decodes JSON data to Key. +func (key *Key) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + parsedKey, err := parseKey(s) + if err != nil { + return err + } + *key = parsedKey + return nil +} + +func parseKey(s string) (Key, error) { + key := Key(s) + + if key.IsValid() { + return key, nil + } + + return key, fmt.Errorf(invalidConditionKey, s) +} + +type KeySet map[Key]struct{} + +func (set KeySet) Add(key Key) { + set[key] = struct{}{} +} + +func (set KeySet) AddAll(keys KeySet) { + for key := range keys { + set[key] = struct{}{} + } + +} + +// returns a key set contains difference of two key set. +// Example: +// keySet1 := ["one", "two", "three"] +// keySet2 := ["two", "four", "three"] +// keySet1.Difference(keySet2) == ["one"] +func (set KeySet) Difference(sset KeySet) KeySet { + nset := make(KeySet) + + for k := range set { + if _, ok := sset[k]; !ok { + nset.Add(k) + } + } + + return nset +} + +// returns whether key set is empty or not. +func (set KeySet) IsEmpty() bool { + return len(set) == 0 +} + +func (set KeySet) String() string { + return fmt.Sprintf("%v", set.ToSlice()) +} + +// ToSlice - returns slice of keys. +func (set KeySet) ToSlice() []Key { + keys := []Key{} + + for key := range set { + keys = append(keys, key) + } + + return keys +} + +// returns new KeySet contains given keys. +func NewKeySet(keys ...Key) KeySet { + set := make(KeySet) + for _, key := range keys { + set.Add(key) + } + + return set +} diff --git a/objectnode/policy_keyset_test.go b/objectnode/policy_keyset_test.go new file mode 100644 index 000000000..22d644262 --- /dev/null +++ b/objectnode/policy_keyset_test.go @@ -0,0 +1,98 @@ +package objectnode + +import ( + "reflect" + "testing" +) + +func TestKeySetAdd(t *testing.T) { + testCases := []struct { + set KeySet + key Key + expectedResult KeySet + }{ + {NewKeySet(), AWSReferer, NewKeySet(AWSReferer)}, + {NewKeySet(AWSReferer), AWSReferer, NewKeySet(AWSReferer)}, + } + + for i, testCase := range testCases { + testCase.set.Add(testCase.key) + + if !reflect.DeepEqual(testCase.expectedResult, testCase.set) { + t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set) + } + } +} + +func TestKeySetDifference(t *testing.T) { + testCases := []struct { + set KeySet + setToDiff KeySet + expectedResult KeySet + }{ + {NewKeySet(), NewKeySet(AWSHost), NewKeySet()}, + {NewKeySet(AWSHost, AWSSourceIP, AWSReferer), NewKeySet(AWSSourceIP, AWSReferer), NewKeySet(AWSHost)}, + } + + for i, testCase := range testCases { + result := testCase.set.Difference(testCase.setToDiff) + + if !reflect.DeepEqual(testCase.expectedResult, result) { + t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } +} + +func TestKeySetIsEmpty(t *testing.T) { + testCases := []struct { + set KeySet + expectedResult bool + }{ + {NewKeySet(), true}, + {NewKeySet(AWSSourceIP), false}, + } + + for i, testCase := range testCases { + result := testCase.set.IsEmpty() + + if testCase.expectedResult != result { + t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } +} + +func TestKeySetString(t *testing.T) { + testCases := []struct { + set KeySet + expectedResult string + }{ + {NewKeySet(), `[]`}, + {NewKeySet(AWSHost), `[aws:Host]`}, + } + + for i, testCase := range testCases { + result := testCase.set.String() + + if testCase.expectedResult != result { + t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } +} + +func TestKeySetToSlice(t *testing.T) { + testCases := []struct { + set KeySet + expectedResult []Key + }{ + {NewKeySet(), []Key{}}, + {NewKeySet(AWSReferer), []Key{AWSReferer}}, + } + + for i, testCase := range testCases { + result := testCase.set.ToSlice() + + if !reflect.DeepEqual(testCase.expectedResult, result) { + t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } +} diff --git a/objectnode/policy_match.go b/objectnode/policy_match.go new file mode 100644 index 000000000..db2873701 --- /dev/null +++ b/objectnode/policy_match.go @@ -0,0 +1,250 @@ +// Copyright 2023 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 ( + "regexp" + "strings" + + "github.com/cubefs/cubefs/util/log" +) + +type ActionElementType string +type ActionType []interface{} +type PrincipalElementType string +type PrincipalType map[string]interface{} +type ResourceElementType string + +func (s *Statement) CheckPolicy(apiName string, uid string, conditionCheck map[string]string) PolicyCheckResult { + if s.match(apiName, uid, conditionCheck) { + return s.effect() + } + return POLICY_UNKNOW +} + +func (s *Statement) effect() PolicyCheckResult { + switch strings.ToLower(s.Effect) { + case "allow": + return POLICY_ALLOW + case "deny": + return POLICY_DENY + default: + return POLICY_UNKNOW + } +} + +func (s *Statement) match(apiName string, uid string, conditionCheck map[string]string) bool { + if !s.matchPrincipal(uid) { + log.LogDebugf("cannot match principal, uid:%v", uid) + return false + } + if !s.matchAction(apiName) { + log.LogDebugf("cannot match action, apiName:%v", apiName) + return false + } + if !s.matchResource(apiName, conditionCheck[KEYNAME]) { + log.LogDebugf("cannot match resource, apiName:%v, keyname:%v", apiName, conditionCheck[KEYNAME]) + return false + } + if !s.matchCondition(conditionCheck) { + log.LogDebugf("cannot match condition, conditionCheck:%v", conditionCheck) + return false + } + return true +} + +//---------------------------------------------------------------------------------------------------------------- +func (s *Statement) matchAction(apiName string) bool { + + log.LogDebug("start to match action") + switch s.Action.(type) { + case []interface{}: //["s3:PutObject", "s3:GetObject","s3:DeleteObject"] + actions := s.Action.([]interface{}) + return ActionType(actions).match(apiName) + + case string: //"s3:ListBucket" + return ActionElementType(s.Action.(string)).match(apiName) + default: + return false + } +} + +func (a ActionElementType) match(apiToMatch string) bool { + a1 := strings.TrimPrefix(strings.ToLower(string(a)), S3_ACTION_PREFIX) + if a1 == ACTION_ANY { + return true + } + for _, action := range S3ActionToApis[a1] { + if action == apiToMatch { + return true + } + } + return false +} + +func (actions ActionType) match(apiToMatch string) bool { + for _, a := range actions { + if a1, ok := a.(string); ok { + if ActionElementType(a1).match(apiToMatch) { + return true + } + } + } + return false +} + +//---------------------------------------------------------------------------------------------------------------- +func (s *Statement) matchPrincipal(uid string) bool { + switch s.Principal.(type) { + case string: // "*" or "123" + return PrincipalElementType(s.Principal.(string)).match(uid) + case map[string]interface{}: //from json, {"AWS":["11", "22"]} or {"AWS":"11"} or {"AWS":"*"} + p := s.Principal.(map[string]interface{}) + return PrincipalType(p).match(uid) + default: + return false + } +} + +func (p PrincipalElementType) match(uid string) bool { + return p == Principal_Any || p == PrincipalElementType(uid) +} + +func (p PrincipalType) match(uid string) bool { + p1, ok := p[S3_PRINCIPAL_PREFIX] + if !ok { + return false + } + switch p1.(type) { + case []interface{}: + p2 := p1.([]interface{}) + for _, p3 := range p2 { + if p4, ok := p3.(string); ok { + if PrincipalElementType(p4).match(uid) { + return true + } + } + } + return false + case string: + return PrincipalElementType(p1.(string)).match(uid) + default: + return false + } +} + +//---------------------------------------------------------------------------------------------------------------- +func (s *Statement) matchResource(apiName string, keyname interface{}) bool { + + if IsBucketApi(apiName) { + return s.matchBucketInResource() + } + + if keyname == nil { //keyname shoudn't be nil for object api, even if keyname = "" + return false + } + if _, ok := keyname.(string); !ok { + return false + } + return s.matchKeyInResource(keyname.(string)) + +} + +func (s *Statement) matchBucketInResource() bool { + //if resource list contains bucket format, then match, since bucketId already checked when put policy. + switch s.Resource.(type) { + case string: + return ResourceElement(s.Resource.(string)).isBucketFormat() + case []interface{}: + r := s.Resource.([]interface{}) + for _, r1 := range r { + if r2, ok := r1.(string); ok { + if ResourceElement(r2).isBucketFormat() { + return true + } + } + } + return false + default: + return false + } + +} + +func (s *Statement) matchKeyInResource(keyname string) bool { //可以处理 keyname="" 的case + + switch s.Resource.(type) { + case string: + return ResourceElement(s.Resource.(string)).match(keyname) + case []interface{}: + r := s.Resource.([]interface{}) + for _, r1 := range r { + if r2, ok := r1.(string); ok { + if ResourceElement(r2).match(keyname) { + return true + } + } + } + return false + default: + return false + } +} + +func ResourceElement(r string) ResourceElementType { + r1 := strings.TrimPrefix(r, S3_RESOURCE_PREFIX) + return ResourceElementType(r1) +} + +func (r ResourceElementType) isBucketFormat() bool { //bucketFormat support bucket api + return !r.isKeyFormat() +} + +func (r ResourceElementType) isKeyFormat() bool { //keyFormat support object api + return strings.Contains(string(r), "/") +} + +func (r ResourceElementType) match(keyname string) bool { + if !r.isKeyFormat() { + return false + } + //extract regex : "examplebucket/abc/*" => rawPattern="abc/*" + r1 := string(r) + i := strings.Index(r1, "/") + rawPattern := r1[i+1:] + if rawPattern == "" { + return false + } + keyPattern := makeRegexPattern(rawPattern) + ok, _ := regexp.MatchString(keyPattern, keyname) + return ok +} + +func makeRegexPattern(raw string) string { + pattern := strings.Replace(raw, "?", ".", -1) + pattern = strings.Replace(pattern, "*", ".*", -1) + pattern = "^" + pattern + "$" + return pattern +} + +//---------------------------------------------------------------------------------------------------------------- +func (s *Statement) matchCondition(conditionCheck map[string]string) bool { + //condition is optional + if s.Condition == nil { + return true + } + return s.Condition.Evaluate(conditionCheck) + +} diff --git a/objectnode/policy_match_test.go b/objectnode/policy_match_test.go new file mode 100644 index 000000000..43209aeea --- /dev/null +++ b/objectnode/policy_match_test.go @@ -0,0 +1,325 @@ +package objectnode + +import ( + "encoding/json" + "regexp" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRegexp(t *testing.T) { + //case1, any keyname + raw := "*" + pattern := makeRegexPattern(raw) + + keynames := []string{"abc", "", " ", "*", ".*", "*.", "**", "/a", "a/", "/a*a", "a/*"} + for _, k := range keynames { + ok, err := regexp.MatchString(pattern, k) + require.NoError(t, err) + require.True(t, ok) + } + + //case2, specify name + raw = "user?/img/*/2019/*" + pattern = makeRegexPattern(raw) + + keynames = []string{"user1/img/jpg/2019/Jan", "user2/img//2019/"} + for _, k := range keynames { + ok, err := regexp.MatchString(pattern, k) + require.NoError(t, err) + require.True(t, ok) + } + keynames = []string{"user1a/img/jpg/2019/Jan", "user2/img/png/2019"} + for _, k := range keynames { + ok, err := regexp.MatchString(pattern, k) + require.NoError(t, err) + require.False(t, ok) + } +} + +func TestResourceMatch(t *testing.T) { + var s Statement + //case1: resource is bucket, specified object(case sensitive) + s.Resource = []interface{}{"arn:aws:s3:::mybucket", "arn:aws:s3:::mybucket/abc/*", "arn:aws:s3:::mybucket/ABD"} + b := s.matchResource(LIST_OBJECTS, nil) + require.True(t, b) + + b = s.matchResource(PUT_OBJECT, "abc/de") + require.True(t, b) + + b = s.matchResource(PUT_OBJECT, "ABD") + require.True(t, b) + + b = s.matchResource(PUT_OBJECT, "abd") + require.False(t, b) + + b = s.matchResource(PUT_OBJECT, "ABC/ab") + require.False(t, b) + + //case2: resource is any object + s.Resource = "arn:aws:s3:::examplebucket/*" + keynames := []string{"", "*", "abc", "/*"} + for _, k := range keynames { + b = s.matchResource(PUT_OBJECT, k) + require.True(t, b) + } + b = s.matchResource(LIST_OBJECTS, "") + require.False(t, b) +} + +func TestPrincipalMatch_MultiUsers(t *testing.T) { + s := &Statement{} + principal := `{"AWS":["11","22"]}` + json.Unmarshal([]byte(principal), &s.Principal) + result := s.matchPrincipal("22") + require.True(t, result) + + result = s.matchPrincipal("33") + require.False(t, result) + + result = s.matchPrincipal("") + require.False(t, result) +} + +func TestPrincipalMatch_Anyone(t *testing.T) { + s := &Statement{} + err := json.Unmarshal([]byte(`"*"`), &s.Principal) + require.NoError(t, err) + result := s.matchPrincipal("22") + require.True(t, result) + result = s.matchPrincipal("") + require.True(t, result) +} + +func TestPrincipalMatch_Anyone_Case2(t *testing.T) { + s := &Statement{} + json.Unmarshal([]byte(`{"AWS":"*"}`), &s.Principal) + result := s.matchPrincipal("22") + require.True(t, result) + result = s.matchPrincipal("") + require.True(t, result) +} + +func TestPrincipalMatch_SpecificalUser(t *testing.T) { + s := &Statement{} + json.Unmarshal([]byte(`{"AWS":"11"}`), &s.Principal) + result := s.matchPrincipal("11") + require.True(t, result) + result = s.matchPrincipal("22") + require.False(t, result) + result = s.matchPrincipal("") + require.False(t, result) +} + +func TestActionMatch_Any(t *testing.T) { + s := &Statement{} + err := json.Unmarshal([]byte(`"s3:*"`), &s.Action) + require.NoError(t, err) + result := s.matchAction(GET_OBJECT) + require.True(t, result) +} + +func TestActionMatch_Any_Mix(t *testing.T) { + s := &Statement{} + err := json.Unmarshal([]byte(`["s3:PutObject","s3:*"]`), &s.Action) + require.NoError(t, err) + result := s.matchAction(GET_OBJECT) + require.True(t, result) +} + +func TestActionMatch_Specifical(t *testing.T) { + s := &Statement{} + err := json.Unmarshal([]byte(`"s3:PutObject"`), &s.Action) + require.NoError(t, err) + result := s.matchAction(PUT_OBJECT) + require.True(t, result) + result = s.matchAction(GET_OBJECT) + require.False(t, result) + + err = json.Unmarshal([]byte(`"s3:DeleteObject"`), &s.Action) + require.NoError(t, err) + result = s.matchAction(DELETE_OBJECT) + require.True(t, result) + result = s.matchAction(BATCH_DELETE) + require.True(t, result) +} + +func TestActionMatch_Multiaction(t *testing.T) { + s := &Statement{} + err := json.Unmarshal([]byte(`["s3:PutObject","s3:GetObject"]`), &s.Action) + require.NoError(t, err) + result := s.matchAction(PUT_OBJECT) + require.True(t, result) + result = s.matchAction(GET_OBJECT) + require.True(t, result) + result = s.matchAction(DELETE_OBJECT) + require.False(t, result) +} + +func TestIpMatch_Whitelist(t *testing.T) { + ipcondition := `{"IpAddress": {"aws:SourceIp": ["1.2.3.4/24", "1.1.1.1","fe80::45e:9d4c:20ca:20f7/64"] }}` + s := &Statement{} + err := json.Unmarshal([]byte(ipcondition), &s.Condition) + require.NoError(t, err) + conditionToCheck := map[string]string{} + + clientIps := []string{"1.2.3.5", "1.1.1.1", "fe80::45e:9d4c:20ca:20f8"} + for _, ip := range clientIps { + conditionToCheck[SOURCEIP] = ip + result := s.matchCondition(conditionToCheck) + require.True(t, result) + } + + conditionToCheck[SOURCEIP] = "1.2.4.5" + result := s.matchCondition(conditionToCheck) + require.False(t, result) +} + +func TestIpMatch_Blacklist(t *testing.T) { + ipcondition := `{"NotIpAddress": {"aws:SourceIp": "1.2.3.4/24" }}` + s := &Statement{} + err := json.Unmarshal([]byte(ipcondition), &s.Condition) + require.NoError(t, err) + conditionToCheck := map[string]string{} + + conditionToCheck[SOURCEIP] = "1.2.3.5" + result := s.matchCondition(conditionToCheck) + require.False(t, result) + + conditionToCheck[SOURCEIP] = "1.2.4.5" + result = s.matchCondition(conditionToCheck) + require.True(t, result) +} + +func TestIpMatch_InWhite_NotInBlack(t *testing.T) { + ipcondition := `{ + "IpAddress": {"aws:SourceIp": ["1.2.3.0/24", "2.2.2.2","4.4.4.4"] }, + "NotIpAddress":{"aws:SourceIp":["1.2.3.5","2.2.2.2","3.3.3.3"] } + }` + s := &Statement{} + err := json.Unmarshal([]byte(ipcondition), &s.Condition) + require.NoError(t, err) + conditionToCheck := map[string]string{} + + //white ip + clientIps := []string{"1.2.3.6", "4.4.4.4"} + for _, ip := range clientIps { + conditionToCheck[SOURCEIP] = ip + result := s.matchCondition(conditionToCheck) + require.True(t, result) + } + //black ip + clientIps = []string{"1.2.3.5", "2.2.2.2", "3.3.3.3"} + for _, ip := range clientIps { + conditionToCheck[SOURCEIP] = ip + result := s.matchCondition(conditionToCheck) + require.False(t, result) + } + +} +func TestStringLikeMatch(t *testing.T) { + strCondition := `{ + "StringLike":{"aws:Referer":["http://*.example.com/*","http://example.com/*"]} + }` + s := &Statement{} + err := json.Unmarshal([]byte(strCondition), &s.Condition) + require.NoError(t, err) + conditionToCheck := map[string]string{} + clientReferer := []string{"http://www.example.com/bucket", "http://example.com/ac", "http://bucekt.example.com/"} + for _, referer := range clientReferer { + conditionToCheck[REFERER] = referer + result := s.matchCondition(conditionToCheck) + require.True(t, result) + } + + clientReferer = []string{"http://www.example.cn/bucket", "http://example.cn/ac"} + for _, referer := range clientReferer { + conditionToCheck[REFERER] = referer + result := s.matchCondition(conditionToCheck) + require.False(t, result) + } + + strCondition = `{ + "StringLike":{"aws:Host":["http://*.example.com/*","http://example.com/*"]} + }` + err = json.Unmarshal([]byte(strCondition), &s.Condition) + require.NoError(t, err) + conditionToCheck = map[string]string{} + clientHost := []string{"http://www.example.com/bucket", "http://example.com/ac", "http://bucekt.example.com/"} + for _, host := range clientHost { + conditionToCheck[HOST] = host + result := s.matchCondition(conditionToCheck) + require.True(t, result) + } + + clientHost = []string{"http://www.example.cn/bucket", "http://example.cn/ac"} + for _, host := range clientHost { + conditionToCheck[HOST] = host + result := s.matchCondition(conditionToCheck) + require.False(t, result) + } + + strCondition = `{"StringLike":{"aws:Referer":"http://*.example.com/*"}}` + err = json.Unmarshal([]byte(strCondition), &s.Condition) + require.NoError(t, err) + clientReferer = []string{"http://www.example.com/bucket", "http://a.example.com/ac"} + for _, referer := range clientReferer { + conditionToCheck[REFERER] = referer + result := s.matchCondition(conditionToCheck) + require.True(t, result) + } +} + +func TestStringNotLikeMatch(t *testing.T) { + strCondition := `{ + "StringNotLike":{"aws:Referer":["http://*.example.com/*","http://example.com/*"]} + }` + s := &Statement{} + err := json.Unmarshal([]byte(strCondition), &s.Condition) + require.NoError(t, err) + conditionToCheck := map[string]string{} + clientReferer := []string{"http://www.example.com/bucket", "http://example.com/ac", "http://bucekt.example.com/"} + for _, referer := range clientReferer { + conditionToCheck[REFERER] = referer + result := s.matchCondition(conditionToCheck) + require.False(t, result) + } + + clientReferer = []string{"http://www.example.cn/bucket", "http://example.cn/ac"} + for _, referer := range clientReferer { + conditionToCheck[REFERER] = referer + result := s.matchCondition(conditionToCheck) + require.True(t, result) + } + + strCondition = `{ + "StringNotLike":{"aws:Host":["http://*.example.com/*","http://example.com/*"]} + }` + err = json.Unmarshal([]byte(strCondition), &s.Condition) + require.NoError(t, err) + conditionToCheck = map[string]string{} + clientHost := []string{"http://www.example.com/bucket", "http://example.com/ac", "http://bucekt.example.com/"} + for _, host := range clientHost { + conditionToCheck[HOST] = host + result := s.matchCondition(conditionToCheck) + require.False(t, result) + } + + clientHost = []string{"http://www.example.cn/bucket", "http://example.cn/ac"} + for _, host := range clientHost { + conditionToCheck[HOST] = host + result := s.matchCondition(conditionToCheck) + require.True(t, result) + } + + strCondition = `{"StringNotLike":{"aws:Referer":"http://*.example.com/*"}}` + err = json.Unmarshal([]byte(strCondition), &s.Condition) + require.NoError(t, err) + clientReferer = []string{"http://www.example.com/bucket", "http://a.example.com/ac"} + for _, referer := range clientReferer { + conditionToCheck[REFERER] = referer + result := s.matchCondition(conditionToCheck) + require.False(t, result) + } +} diff --git a/objectnode/policy_statement.go b/objectnode/policy_statement.go index 0b6a7b8f9..d9042f9e4 100644 --- a/objectnode/policy_statement.go +++ b/objectnode/policy_statement.go @@ -17,29 +17,37 @@ package objectnode +import ( + "strings" + + "github.com/cubefs/cubefs/util/log" +) + // https://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html // https://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html //https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/example-bucket-policies.html -type Effect string type Principal map[string]StringSet type Resource string const ( - Allow Effect = "Allow" - Deny = "Deny" + Allow = "Allow" + Deny = "Deny" ) type Statement struct { - Sid string `json:"Sid,omitempty"` - Effect Effect `json:"Effect"` - Principal Principal `json:"Principal"` - Actions StringSet `json:"Action,omitempty"` - NotActions StringSet `json:"NotAction,omitempty"` - Resources StringSet `json:"Resource,omitempty"` - NotResources StringSet `json:"NotResource,omitempty"` - Condition Condition `json:"Condition,omitempty"` + Sid string `json:"Sid"` + Effect string `json:"Effect"` + Principal interface{} `json:"Principal"` // map or string + Action interface{} `json:"Action"` // []string or string + Resource interface{} `json:"Resource"` // []string or string + Condition Condition `json:"Condition,omitempty"` +} + +func (s Statement) IsAllowed(p *RequestParam) bool { + + return s.Effect == Allow } func (s *Statement) Validate(bucket string) (bool, error) { @@ -47,71 +55,260 @@ func (s *Statement) Validate(bucket string) (bool, error) { } func (s *Statement) isValid(bucket string) (bool, error) { - //TODO: + log.LogDebug("start to validate statement") + // step 1: check required field + if err := s.checkRequiredField(); err != nil { + return false, err + } + //step2: check each field + if !s.isEffectValid() { + return false, ErrInvalidEffectValue + } + + if !s.isPrincipalValid() { + return false, ErrInvalidPricipalInPolicy + } + + if !s.isActionValid() { + return false, ErrInvalidActionInPolicy + } + + if !s.isResourceValid(bucket) { + return false, ErrInvalidResourceInPolicy + } + + //step3: check action & resource valid combination + if !s.isValidCombination() { + return false, ErrInvalidActionResourceCombination + } return true, nil } -func (s Statement) IsAllowed(p *RequestParam) bool { - checked := s.check(p) - - if s.Effect != Allow { - return !checked +func (s *Statement) checkRequiredField() error { + switch { + case s.Effect == "": + return ErrMissingEffectInPolicy + case s.Principal == nil: + return ErrMissingPrincipalInPolicy + case s.Action == nil: + return ErrMissingActionInPolicy + case s.Resource == nil: + return ErrMissingResourceInPolicy + default: + return nil } - - return checked } -type CheckFuncs func(p *RequestParam) bool - -func (s Statement) check(p *RequestParam) bool { - checkFuncs := []CheckFuncs{ - s.checkPrincipal, - s.checkNotActions, - s.checkActions, - s.checkNotResources, - s.checkResources, - s.checkConditions, +func (s *Statement) isEffectValid() bool { + e := strings.ToLower(s.Effect) + if e == "allow" || e == "deny" { + return true } - for _, f := range checkFuncs { - checked := f(p) - if !checked { + return false +} + +// "Principal": "*" or "Principal" : {"AWS":"111122223333"} or "Principal" : {"AWS":["111122223333","444455556666"]} +func (s *Statement) isPrincipalValid() bool { + //principal: uid must be "*" or uint32, and can't be 0 + switch s.Principal.(type) { + case string: // "*" or "123" + p := s.Principal.(string) + if !PrincipalElementType(p).valid() { return false } - } - - return true -} - -func (s Statement) checkPrincipal(p *RequestParam) bool { - if len(s.Principal) == 0 { - return true - } - for _, principal := range s.Principal { - if principal.ContainsWild(p.AccessKey()) { - return true + case map[string]interface{}: + p := s.Principal.(map[string]interface{}) + if !PrincipalType(p).valid() { + return false } - } - - return false -} - -func (s Statement) checkResources(p *RequestParam) bool { - if s.Resources.Empty() { - return true - } - if s.Resources.ContainsRegex(p.resource) { - return true - } - return false -} - -func (s Statement) checkNotResources(p *RequestParam) bool { - if s.NotResources.Empty() { - return true - } - if s.NotResources.ContainsRegex(p.resource) { + default: return false } return true } + +func (p PrincipalType) valid() bool { + p1, ok := p[S3_PRINCIPAL_PREFIX] + if !ok { + return false + } + switch p1.(type) { + case []interface{}: + p2 := p1.([]interface{}) + if len(p2) == 0 { + return false + } + for _, p3 := range p2 { + p4, ok := p3.(string) + if !ok { + return false + } + if !PrincipalElementType(p4).valid() { + return false + } + } + return true + case string: + if !PrincipalElementType(p1.(string)).valid() { + return false + } + default: + return false + } + return true +} + +func (p PrincipalElementType) valid() bool { + return true +} + +func (s *Statement) isActionValid() bool { + switch s.Action.(type) { + case []interface{}: //["s3:PutObject", "s3:GetObject","s3:DeleteObject"] + actions := s.Action.([]interface{}) + return ActionType(actions).valid() + case string: //"s3:ListBucket" + action := s.Action.(string) + return ActionElementType(action).valid() + default: + return false + } +} + +func (a ActionElementType) valid() bool { + a1 := strings.TrimPrefix(strings.ToLower(string(a)), S3_ACTION_PREFIX) + if a1 == ACTION_ANY { + return true + } + _, ok := S3ActionToApis[a1] + return ok +} + +func (actions ActionType) valid() bool { + if len(actions) == 0 { + return false + } + for _, a := range actions { + a1, ok := a.(string) + if !ok { + return false + } + if !ActionElementType(a1).valid() { + return false + } + } + return true +} + +func (s *Statement) isResourceValid(bucketId string) bool { + switch s.Resource.(type) { + case string: // "Resource":"arn:aws:s3:::bucket/*" + r := s.Resource.(string) + return ResourceElement(r).valid(bucketId) + case []interface{}: // "Resource":["arn:aws:s3:::bucket/abc/*"] + r := s.Resource.([]interface{}) + if len(r) == 0 { + return false + } + for _, r1 := range r { + r2, ok := r1.(string) + if !ok { + return false + } + if !ResourceElement(r2).valid(bucketId) { + return false + } + } + return true + default: + return false + } +} + +func (r ResourceElementType) valid(bucketId string) bool { + bucket_key := strings.SplitN(string(r), "/", 2) + if len(bucket_key) < 2 { + return r == ResourceElementType(bucketId) + } + if bucket_key[0] != bucketId { //bucketId in resource list must be same with current bucketId + return false + } + if bucket_key[1] == "" { // key can't be empty when bucket is followed by a slash + return false + } + return true +} + +func (s *Statement) isValidCombination() bool { + //check action & resource valid combination + hasBucketFormat, hasObjectFormat := s.getResourceFormat() + switch s.Action.(type) { + case string: + action := s.Action.(string) + return ActionElementType(action).matchResource(hasBucketFormat, hasObjectFormat) + case []interface{}: + actions := s.Action.([]interface{}) + return ActionType(actions).matchResource(hasBucketFormat, hasObjectFormat) + default: + return false + } +} + +func (a ActionElementType) matchResource(hasBucketFormat, hasObjectFormat bool) bool { + a1 := strings.TrimPrefix(strings.ToLower(string(a)), S3_ACTION_PREFIX) + if a1 == ACTION_ANY { // when action = "*", resource can contain "bucket" or "bucket/key" + return hasBucketFormat || hasObjectFormat + } + if validBucketActions.Contain(string(a1)) { + return hasBucketFormat + } + return hasObjectFormat +} + +func (actions ActionType) matchResource(hasBucketFormat, hasObjectFormat bool) bool { + if len(actions) == 0 { + return false + } + for _, a := range actions { //every action should match + if a1, ok := a.(string); ok { + if !ActionElementType(a1).matchResource(hasBucketFormat, hasObjectFormat) { + return false + } + } + } + return true +} + +func (s *Statement) getResourceFormat() (hasBucketFormat, hasObjectFormat bool) { + switch s.Resource.(type) { + case string: + r := s.Resource.(string) + return ResourceElement(r).format() + case []interface{}: + r := s.Resource.([]interface{}) + if len(r) == 0 { + return + } + for _, r1 := range r { + if r2, ok := r1.(string); ok { + isBucketFormat, isKeyFormat := ResourceElement(r2).format() + if isBucketFormat { + hasBucketFormat = true + } + if isKeyFormat { + hasObjectFormat = true + } + } + } + return + default: + return false, false + } +} + +func (r ResourceElementType) format() (isBucketFormat, isKeyFormat bool) { + isBucketFormat = r.isBucketFormat() + isKeyFormat = r.isKeyFormat() + return +} diff --git a/objectnode/policy_statement_test.go b/objectnode/policy_statement_test.go new file mode 100644 index 000000000..042787a09 --- /dev/null +++ b/objectnode/policy_statement_test.go @@ -0,0 +1,311 @@ +package objectnode + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPolicyExample(t *testing.T) { + policyJson := `{ + "Version": "2012-10-17", + "Id": "ExamplePolicy01", + "Statement": [ + { + "Sid": "ExampleStatement01", + "Effect": "Allow", + "Principal": { + "AWS": ["11","22"] + }, + "Action": [ + "s3:GetObject", + "s3:GetBucketLocation", + "s3:ListBucket" + ], + "Resource": [ + "arn:aws:s3:::examplebucket/*", + "arn:aws:s3:::examplebucket" + ], + "Condition": { + "StringLike": { + "aws:Referer": [ + "*.cubefs.com" + ] + } + } + } + ] + }` + var policy Policy + err := json.Unmarshal([]byte(policyJson), &policy) + require.NoError(t, err) + out, err := json.Marshal(policy) + err = json.Unmarshal(out, &policy) + require.NoError(t, err) + +} + +func TestMissingRequiredField(t *testing.T) { + policyJson := `{ + "Version": "2008-10-17", + "Statement": [ + { + "Principal": { + "AWS": "099454345549" + }, + "action": [ + "s3:ListBucket", + "s3:getobject" + ], + "Resource": [ + "arn:aws:s3:::examplebucket/*", + "arn:aws:s3:::examplebucket" + ] + } + ] + }` + var out1 Policy + dec := json.NewDecoder(strings.NewReader(policyJson)) + dec.DisallowUnknownFields() + err := dec.Decode(&out1) + require.NoError(t, err) + _, err = out1.Validate("examplebucket") + require.EqualError(t, err, "missing Effect in policy") + + policyJson = `{ + "Version": "", + "Statement": [ + { + "Principal": { + "AWS": "099454345549" + }, + "Effect": "Allow", + "action": [ + "s3:ListBucket", + "s3:getobject" + ], + "Resource": [ + "arn:aws:s3:::examplebucket/*", + "arn:aws:s3:::examplebucket" + ] + } + ] + }` + var out2 Policy + dec = json.NewDecoder(strings.NewReader(policyJson)) + dec.DisallowUnknownFields() + err = dec.Decode(&out2) + require.NoError(t, err) + _, err = out2.Validate("examplebucket") + require.EqualError(t, err, "missing Version in policy") + + policyJson = `{ + "Version": "2008-10-17" + }` + var out3 Policy + dec = json.NewDecoder(strings.NewReader(policyJson)) + dec.DisallowUnknownFields() + err = dec.Decode(&out3) + require.NoError(t, err) + _, err = out3.Validate("examplebucket") + require.EqualError(t, err, "missing Statement in policy") +} + +func TestEffectFormat(t *testing.T) { + var s Statement + json.Unmarshal([]byte(`"dummy"`), &s.Effect) + b := s.isEffectValid() + require.False(t, b) + + json.Unmarshal([]byte(`"alloW"`), &s.Effect) + b = s.isEffectValid() + require.True(t, b) + + json.Unmarshal([]byte(`"Deny"`), &s.Effect) + b = s.isEffectValid() + require.True(t, b) + +} + +func TestResourceFormat(t *testing.T) { + //bad: "arn:aws:s3:::bucket/" , or "arn:aws:s3:::/keyname" or "arn:aws:s3:::/" or "arn:aws:s3:::wrongbucket/key" or [ ] empty + bucketId := "examplebucket" + var s Statement + + validResources := []string{ + `["arn:aws:s3:::examplebucket"]`, + `["arn:aws:s3:::examplebucket/*"]`, + `["arn:aws:s3:::examplebucket/abc"]`, + `["arn:aws:s3:::examplebucket/abc/*"]`, + `"arn:aws:s3:::examplebucket/abc?d/*"`, + `"arn:aws:s3:::examplebucket"`, + `"arn:aws:s3:::examplebucket/*"`, + } + + for _, r := range validResources { + err := json.Unmarshal([]byte(r), &s.Resource) + require.NoError(t, err) + b := s.isResourceValid(bucketId) + require.True(t, b) + } + + invalidResources := []string{ + `["arn:aws:s3:::examplebucket/","arn:aws:s3:::examplebucket/key"]`, //first one is invaid + `["arn:aws:s3:::*/keyname"]`, + `["arn:aws:s3:::/keyname"]`, + `["arn:aws:s3:::/"]`, + `["arn:aws:s3:::/*"]`, + `["arn:aws:s3:::wrongbucket/key"]`, + `["arn:aws:s3:::wrongbucket"]`, + `["Arn:aws:s3:::examplebucket"]`, //Arn is invalid, should be arn + `[]`, + `""`, + `"arn:aws:s3:::exampl*bucket"`, + `"arn:aws:s3:::*"`, + `["arn:aws:s3:::*"]`, + } + + for _, r := range invalidResources { + err := json.Unmarshal([]byte(r), &s.Resource) + require.NoError(t, err) + b := s.isResourceValid(bucketId) + require.False(t, b) + } +} + +func TestPrincipalFormat(t *testing.T) { + var s Statement + b := s.isPrincipalValid() + require.False(t, b) + + validPrincipal := []string{ + `{"AWS":["11","22"]}`, + `{"AWS":"11"}`, + `{"AWS":"*"}`, + `"*"`, + `"11"`, + } + for _, p := range validPrincipal { + err := json.Unmarshal([]byte(p), &s.Principal) + require.NoError(t, err) + b := s.isPrincipalValid() + require.True(t, b) + } + + invalidPrincipal := []string{ + `{}`, + `{"aws":"11"}`, //must be "AWS", not "aws" + `{"AWS":11}`, //must be string, not number, 11 should be "11" + `{"AWS":[]}`, + `{"AWS":["11",22]}`, + `["11"]`, //currently, aws not support [] for principal + } + for _, p := range invalidPrincipal { + err := json.Unmarshal([]byte(p), &s.Principal) + require.NoError(t, err) + b := s.isPrincipalValid() + require.False(t, b) + } +} + +func TestActionFormat(t *testing.T) { + var s Statement + validAction := []string{ + `["s3:PutObject","s3:getobject","s3:deleteObject","s3:AbortMultipartUpload","s3:ListMultipartUploadParts"]`, + `["S3:ListBucket","S3:deletebucket","s3:listbucketMultipartUploads"]`, + `["s3:*"]`, + } + for _, a := range validAction { + err := json.Unmarshal([]byte(a), &s.Action) + require.NoError(t, err) + b := s.isActionValid() + require.True(t, b) + } + + invalidAction := []string{ + `["s3:CreateBucket"]`, + `["s3:ListAllMyBuckets"]`, + `["s3:DeleteBucketPolicy"]`, + `["s3:GetBucketPolicy"]`, + `["s3:PutBucketPolicy"]`, + `[]`, + `["s3:PutObject","s3:invalid"]`, //second is invalid + } + for _, a := range invalidAction { + err := json.Unmarshal([]byte(a), &s.Action) + require.NoError(t, err) + b := s.isActionValid() + require.False(t, b) + } +} + +func TestConditionFormat(t *testing.T) { + + validCondition := []string{ + `{"IpAddress":{"aws:SourceIp":["1.1.1.1/24","1.1.1.1","fe80::45e:9d4c:20ca:20f7/64"]}}`, + `{"NotIpAddress":{"aws:SourceIp":"1.1.1.3"}}`, + `{"StringLike":{"aws:Referer":"http://*.example.com/*"}}`, + `{"StringLike":{"aws:Referer":["http://*.example.com/*","http://example.com/*"]}}`, + `{"StringLike":{"aws:Host":"http://*.example.com/*"}}`, + `{"StringLike":{"aws:Host":["http://*.example.com/*","http://example.com/*"]}}`, + `{"StringNotLike":{"aws:Referer":"http://*.example.com/*"}}`, + `{"StringNotLike":{"aws:Referer":["http://*.example.com/*","http://example.com/*"]}}`, + `{"StringNotLike":{"aws:Host":"http://*.example.com/*"}}`, + `{"StringNotLike":{"aws:Host":["http://*.example.com/*","http://example.com/*"]}}`, + } + for _, c := range validCondition { + var cond Condition + err := json.Unmarshal([]byte(c), &cond) + require.NoError(t, err) + } + + invalidCondition := []string{ + `{"ipAddress":{"aws:SourceIp":["1.1.1.1/24"]}}`, //should be "IpAddress" + `{"notIpAddress":{"aws:SourceIp":["1.1.1.3"]}}`, //should be "NotIpAddress" + `{"IpAddress":{"SourceIp":["1.1.1.3"]}}`, //should be "aws:SourceIp" + `{"StringLik":{"aws:Referer":"http://*.example.com/*"}}`, //should be "aws:StringLike" + `{"StringLike":{"aws:SourceIP":"http://*.example.com/*"}}`, + `{"StringLike":{"aws:referer":"http://*.example.com/*"}}`, + `{"StringLike":{"aws:host":"http://*.example.com/*"}}`, + `{"StringNotLik":{"aws:Referer":"http://*.example.com/*"}}`, //should be "aws:StringLike" + `{"StringNotLike":{"aws:SourceIP":"http://*.example.com/*"}}`, + `{"StringNotLike":{"aws:referer":"http://*.example.com/*"}}`, + `{"StringNotLike":{"aws:host":"http://*.example.com/*"}}`, + } + for _, c := range invalidCondition { + var cond Condition + err := json.Unmarshal([]byte(c), &cond) + require.Error(t, err) + + } +} + +func TestActionResourceCombination(t *testing.T) { + var s Statement + var resource_action = []struct { + action string + resource string + matchResult bool + }{ + {`["s3:*"]`, `["arn:aws:s3:::examplebucket"]`, true}, + {`["s3:*"]`, `["arn:aws:s3:::examplebucket/*"]`, true}, + {`["s3:ListBucket"]`, `["arn:aws:s3:::examplebucket"]`, true}, + {`["s3:GetObject"]`, `["arn:aws:s3:::examplebucket/*"]`, true}, + {`["s3:ListBucket"]`, `["arn:aws:s3:::examplebucket","arn:aws:s3:::examplebucket/*"]`, true}, + {`["s3:GetObject"]`, `["arn:aws:s3:::examplebucket","arn:aws:s3:::examplebucket/*"]`, true}, + {`["s3:GetObject","s3:ListBucket"]`, `["arn:aws:s3:::examplebucket","arn:aws:s3:::examplebucket/*"]`, true}, + + {`["s3:GetObject"]`, `["arn:aws:s3:::examplebucket"]`, false}, + {`["s3:ListBucket"]`, `["arn:aws:s3:::examplebucket/*"]`, false}, + {`["s3:GetObject","s3:ListBucket"]`, `["arn:aws:s3:::examplebucket"]`, false}, + {`["s3:GetObject","s3:ListBucket"]`, `["arn:aws:s3:::examplebucket/*"]`, false}, + } + for _, ra := range resource_action { + json.Unmarshal([]byte(ra.resource), &s.Resource) + json.Unmarshal([]byte(ra.action), &s.Action) + b := s.isValidCombination() + require.Equal(t, ra.matchResult, b) + } +} diff --git a/objectnode/policy_stringlikeop.go b/objectnode/policy_stringlikeop.go new file mode 100644 index 000000000..76c314336 --- /dev/null +++ b/objectnode/policy_stringlikeop.go @@ -0,0 +1,201 @@ +// Copyright 2023 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 ( + "fmt" + "net/http" +) + +// String like operation. It checks whether value by Key in given +// values map is wildcard matching in condition values. +// For example, +// - if values = ["mybucket/foo*"], at evaluate() it returns whether string +// in value map for Key is wildcard matching in values. +type stringLikeOp struct { + m map[Key]StringSet +} + +// evaluates to check whether value by Key in given values is wildcard +// matching in condition values. +func (op stringLikeOp) evaluate(values map[string]string) bool { + for k, v := range op.m { + requestValue, ok := values[http.CanonicalHeaderKey(k.Name())] + if !ok { + requestValue = values[k.Name()] + } + nothingMatched := true //all values not matched + for p := range v { + if Match(p, requestValue) { + nothingMatched = false + } + } + if nothingMatched { + return false + } + } + + return true +} + +// returns condition key which is used by this condition operation. +func (op stringLikeOp) keys() KeySet { + keys := make(KeySet) + for key := range op.m { + keys.Add(key) + } + return keys +} + +// returns "StringLike" operator. +func (op stringLikeOp) operator() operator { + return stringLike +} + +// returns map representation of this operation. +func (op stringLikeOp) toMap() map[Key]ValueSet { + resultMap := make(map[Key]ValueSet) + for k, v := range op.m { + if !k.IsValid() { + return nil + } + values := NewValueSet() + for _, value := range v.ToSlice() { + values.Add(NewStringValue(value)) + } + resultMap[k] = values + } + + return resultMap +} + +// returns new StringLike operation. +func newStringLikeOp(m map[Key]ValueSet) (Operation, error) { + newMap, err := parseMap(m, stringLike) + if err != nil { + return nil, err + } + return NewStringLikeOp(newMap) +} + +// NewStringLikeOp - returns new StringLike operation. +func NewStringLikeOp(m map[Key]StringSet) (Operation, error) { + return &stringLikeOp{m: m}, nil +} + +func parseMap(m map[Key]ValueSet, op operator) (map[Key]StringSet, error) { + newMap := make(map[Key]StringSet) + for k, v := range m { + valueStrings, err := valuesToStringSlice(op, v) + if err != nil { + return nil, err + } + sset := CreateStringSet(valueStrings...) + newMap[k] = sset + } + return newMap, nil +} + +// stringNotLikeOp - String not like operation. It checks whether value by Key in given +// values map is NOT wildcard matching in condition values. +// For example, +// - if values = ["mybucket/foo*"], at evaluate() it returns whether string +// in value map for Key is NOT wildcard matching in values. +type stringNotLikeOp struct { + stringLikeOp +} + +// evaluates to check whether value by Key in given values is NOT wildcard +// matching in condition values. +func (op stringNotLikeOp) evaluate(values map[string]string) bool { + return !op.stringLikeOp.evaluate(values) +} + +// returns "StringNotLike" function operator. +func (op stringNotLikeOp) operator() operator { + return stringNotLike +} + +// returns new StringNotLike operation. +func newStringNotLikeOp(m map[Key]ValueSet) (Operation, error) { + newMap, err := parseMap(m, stringNotLike) + if err != nil { + return nil, err + } + + return NewStringNotLikeOp(newMap) +} + +// returns new StringNotLike operation. +func NewStringNotLikeOp(m map[Key]StringSet) (Operation, error) { + + return &stringNotLikeOp{stringLikeOp{m}}, nil +} + +func valuesToStringSlice(op operator, values ValueSet) ([]string, error) { + var valueStrings []string + + for value := range values { + // if AWS supports non-string values, we would need to support it. + s, err := value.GetString() + if err != nil { + return nil, fmt.Errorf(invalidStringValue, op) + } + + valueStrings = append(valueStrings, s) + } + + return valueStrings, nil +} + +func Match(pattern, name string) (matched bool) { + if pattern == "" { + return name == pattern + } + if pattern == "*" { + return true + } + rname := make([]rune, 0, len(name)) + rpattern := make([]rune, 0, len(pattern)) + for _, r := range name { + rname = append(rname, r) + } + for _, r := range pattern { + rpattern = append(rpattern, r) + } + simple := false // Does extended wildcard '*' and '?' match. + return deepMatchRune(rname, rpattern, simple) +} + +func deepMatchRune(str, pattern []rune, simple bool) bool { + for len(pattern) > 0 { + switch pattern[0] { + default: + if len(str) == 0 || str[0] != pattern[0] { + return false + } + case '?': + if len(str) == 0 && !simple { + return false + } + case '*': + return deepMatchRune(str, pattern[1:], simple) || + (len(str) > 0 && deepMatchRune(str[1:], pattern, simple)) + } + str = str[1:] + pattern = pattern[1:] + } + return len(str) == 0 && len(pattern) == 0 +} diff --git a/objectnode/policy_stringlikeop_test.go b/objectnode/policy_stringlikeop_test.go new file mode 100644 index 000000000..78d21bd0f --- /dev/null +++ b/objectnode/policy_stringlikeop_test.go @@ -0,0 +1,457 @@ +package objectnode + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestStringLikeOpEvaluate(t *testing.T) { + case1Operation, err := newStringLikeOp(map[Key]ValueSet{AWSReferer: NewValueSet(NewStringValue("*.cubefs.com"), NewStringValue("cubefs.com"))}) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case2Operation, err := newStringLikeOp(map[Key]ValueSet{AWSReferer: NewValueSet(NewStringValue("cubefs.com"))}) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case3Operation, err := newStringLikeOp(map[Key]ValueSet{AWSHost: NewValueSet(NewStringValue("*.example.com"))}) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + testCases := []struct { + operation Operation + values map[string]string + expectedResult bool + }{ + {case1Operation, map[string]string{"Referer": "www.s3.com"}, false}, + {case1Operation, map[string]string{"Referer": "cubefs.com.cn"}, false}, + {case1Operation, map[string]string{"Referer": "www.cubefs.com"}, true}, + {case1Operation, map[string]string{"Referer": "cubefs.com"}, true}, + + {case2Operation, map[string]string{"Referer": "www.cubefs.com"}, false}, + {case2Operation, map[string]string{"Referer": "cubefs.com"}, true}, + {case2Operation, map[string]string{"Referer": "cubefs.cn"}, false}, + + {case3Operation, map[string]string{"Host": "www.example.com"}, true}, + {case3Operation, map[string]string{"Host": "example.com"}, false}, + {case3Operation, map[string]string{"Host": "example.cn"}, false}, + } + + for i, testCase := range testCases { + result := testCase.operation.evaluate(testCase.values) + + if result != testCase.expectedResult { + t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } +} +func TestStringNotLikeOpEvaluate(t *testing.T) { + case1Operation, err := newStringNotLikeOp(map[Key]ValueSet{AWSReferer: NewValueSet(NewStringValue("*.cubefs.com"), NewStringValue("cubefs.com"))}) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case2Operation, err := newStringNotLikeOp(map[Key]ValueSet{AWSReferer: NewValueSet(NewStringValue("cubefs.com"))}) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + case3Operation, err := newStringNotLikeOp(map[Key]ValueSet{AWSHost: NewValueSet(NewStringValue("*.example.com"))}) + if err != nil { + t.Fatalf("unexpected error. %v\n", err) + } + + testCases := []struct { + operation Operation + values map[string]string + expectedResult bool + }{ + {case1Operation, map[string]string{"Referer": "www.s3.com"}, true}, + {case1Operation, map[string]string{"Referer": "cubefs.com.cn"}, true}, + {case1Operation, map[string]string{"Referer": "www.cubefs.com"}, false}, + {case1Operation, map[string]string{"Referer": "cubefs.com"}, false}, + {case2Operation, map[string]string{"Referer": "www.cubefs.com"}, true}, + {case2Operation, map[string]string{"Referer": "cubefs.com"}, false}, + {case2Operation, map[string]string{"Referer": "cubefs.cn"}, true}, + + {case3Operation, map[string]string{"Host": "www.example.com"}, false}, + {case3Operation, map[string]string{"Host": "example.com"}, true}, + {case3Operation, map[string]string{"Host": "example.cn"}, true}, + } + + for i, testCase := range testCases { + result := testCase.operation.evaluate(testCase.values) + + if result != testCase.expectedResult { + t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } +} + +func TestMatch(t *testing.T) { + testCases := []struct { + pattern string + text string + matched bool + }{ + // Test case - 1. + // Test case with pattern "*". Expected to match any text. + { + pattern: "*", + text: "s3:GetObject", + matched: true, + }, + // Test case - 2. + // Test case with empty pattern. This only matches empty string. + { + pattern: "", + text: "s3:GetObject", + matched: false, + }, + // Test case - 3. + // Test case with empty pattern. This only matches empty string. + { + pattern: "", + text: "", + matched: true, + }, + // Test case - 4. + // Test case with single "*" at the end. + { + pattern: "s3:*", + text: "s3:ListMultipartUploadParts", + matched: true, + }, + // Test case - 5. + // Test case with a no "*". In this case the pattern and text should be the same. + { + pattern: "s3:ListBucketMultipartUploads", + text: "s3:ListBucket", + matched: false, + }, + // Test case - 6. + // Test case with a no "*". In this case the pattern and text should be the same. + { + pattern: "s3:ListBucket", + text: "s3:ListBucket", + matched: true, + }, + // Test case - 7. + // Test case with a no "*". In this case the pattern and text should be the same. + { + pattern: "s3:ListBucketMultipartUploads", + text: "s3:ListBucketMultipartUploads", + matched: true, + }, + // Test case - 8. + // Test case with pattern containing key name with a prefix. Should accept the same text without a "*". + { + pattern: "my-bucket/oo*", + text: "my-bucket/oo", + matched: true, + }, + // Test case - 9. + // Test case with "*" at the end of the pattern. + { + pattern: "my-bucket/In*", + text: "my-bucket/India/Karnataka/", + matched: true, + }, + // Test case - 10. + // Test case with prefixes shuffled. + // This should fail. + { + pattern: "my-bucket/In*", + text: "my-bucket/Karnataka/India/", + matched: false, + }, + // Test case - 11. + // Test case with text expanded to the wildcards in the pattern. + { + pattern: "my-bucket/In*/Ka*/Ban", + text: "my-bucket/India/Karnataka/Ban", + matched: true, + }, + // Test case - 12. + // Test case with the keyname part is repeated as prefix several times. + // This is valid. + { + pattern: "my-bucket/In*/Ka*/Ban", + text: "my-bucket/India/Karnataka/Ban/Ban/Ban/Ban/Ban", + matched: true, + }, + // Test case - 13. + // Test case to validate that `*` can be expanded into multiple prefixes. + { + pattern: "my-bucket/In*/Ka*/Ban", + text: "my-bucket/India/Karnataka/Area1/Area2/Area3/Ban", + matched: true, + }, + // Test case - 14. + // Test case to validate that `*` can be expanded into multiple prefixes. + { + pattern: "my-bucket/In*/Ka*/Ban", + text: "my-bucket/India/State1/State2/Karnataka/Area1/Area2/Area3/Ban", + matched: true, + }, + // Test case - 15. + // Test case where the keyname part of the pattern is expanded in the text. + { + pattern: "my-bucket/In*/Ka*/Ban", + text: "my-bucket/India/Karnataka/Bangalore", + matched: false, + }, + // Test case - 16. + // Test case with prefixes and wildcard expanded for all "*". + { + pattern: "my-bucket/In*/Ka*/Ban*", + text: "my-bucket/India/Karnataka/Bangalore", + matched: true, + }, + // Test case - 17. + // Test case with keyname part being a wildcard in the pattern. + { + pattern: "my-bucket/*", + text: "my-bucket/India", + matched: true, + }, + // Test case - 18. + { + pattern: "my-bucket/oo*", + text: "my-bucket/odo", + matched: false, + }, + + // Test case with pattern containing wildcard '?'. + // Test case - 19. + // "my-bucket?/" matches "my-bucket1/", "my-bucket2/", "my-bucket3" etc... + // doesn't match "mybucket/". + { + pattern: "my-bucket?/abc*", + text: "mybucket/abc", + matched: false, + }, + // Test case - 20. + { + pattern: "my-bucket?/abc*", + text: "my-bucket1/abc", + matched: true, + }, + // Test case - 21. + { + pattern: "my-?-bucket/abc*", + text: "my--bucket/abc", + matched: false, + }, + // Test case - 22. + { + pattern: "my-?-bucket/abc*", + text: "my-1-bucket/abc", + matched: true, + }, + // Test case - 23. + { + pattern: "my-?-bucket/abc*", + text: "my-k-bucket/abc", + matched: true, + }, + // Test case - 24. + { + pattern: "my??bucket/abc*", + text: "mybucket/abc", + matched: false, + }, + // Test case - 25. + { + pattern: "my??bucket/abc*", + text: "my4abucket/abc", + matched: true, + }, + // Test case - 26. + { + pattern: "my-bucket?abc*", + text: "my-bucket/abc", + matched: true, + }, + // Test case 27-28. + // '?' matches '/' too. (works with s3). + // This is because the namespace is considered flat. + // "abc?efg" matches both "abcdefg" and "abc/efg". + { + pattern: "my-bucket/abc?efg", + text: "my-bucket/abcdefg", + matched: true, + }, + { + pattern: "my-bucket/abc?efg", + text: "my-bucket/abc/efg", + matched: true, + }, + // Test case - 29. + { + pattern: "my-bucket/abc????", + text: "my-bucket/abc", + matched: false, + }, + // Test case - 30. + { + pattern: "my-bucket/abc????", + text: "my-bucket/abcde", + matched: false, + }, + // Test case - 31. + { + pattern: "my-bucket/abc????", + text: "my-bucket/abcdefg", + matched: true, + }, + // Test case 32-34. + // test case with no '*'. + { + pattern: "my-bucket/abc?", + text: "my-bucket/abc", + matched: false, + }, + { + pattern: "my-bucket/abc?", + text: "my-bucket/abcd", + matched: true, + }, + { + pattern: "my-bucket/abc?", + text: "my-bucket/abcde", + matched: false, + }, + // Test case 35. + { + pattern: "my-bucket/mnop*?", + text: "my-bucket/mnop", + matched: false, + }, + // Test case 36. + { + pattern: "my-bucket/mnop*?", + text: "my-bucket/mnopqrst/mnopqr", + matched: true, + }, + // Test case 37. + { + pattern: "my-bucket/mnop*?", + text: "my-bucket/mnopqrst/mnopqrs", + matched: true, + }, + // Test case 38. + { + pattern: "my-bucket/mnop*?", + text: "my-bucket/mnop", + matched: false, + }, + // Test case 39. + { + pattern: "my-bucket/mnop*?", + text: "my-bucket/mnopq", + matched: true, + }, + // Test case 40. + { + pattern: "my-bucket/mnop*?", + text: "my-bucket/mnopqr", + matched: true, + }, + // Test case 41. + { + pattern: "my-bucket/mnop*?and", + text: "my-bucket/mnopqand", + matched: true, + }, + // Test case 42. + { + pattern: "my-bucket/mnop*?and", + text: "my-bucket/mnopand", + matched: false, + }, + // Test case 43. + { + pattern: "my-bucket/mnop*?and", + text: "my-bucket/mnopqand", + matched: true, + }, + // Test case 44. + { + pattern: "my-bucket/mnop*?", + text: "my-bucket/mn", + matched: false, + }, + // Test case 45. + { + pattern: "my-bucket/mnop*?", + text: "my-bucket/mnopqrst/mnopqrs", + matched: true, + }, + // Test case 46. + { + pattern: "my-bucket/mnop*??", + text: "my-bucket/mnopqrst", + matched: true, + }, + // Test case 47. + { + pattern: "my-bucket/mnop*qrst", + text: "my-bucket/mnopabcdegqrst", + matched: true, + }, + // Test case 48. + { + pattern: "my-bucket/mnop*?and", + text: "my-bucket/mnopqand", + matched: true, + }, + // Test case 49. + { + pattern: "my-bucket/mnop*?and", + text: "my-bucket/mnopand", + matched: false, + }, + // Test case 50. + { + pattern: "my-bucket/mnop*?and?", + text: "my-bucket/mnopqanda", + matched: true, + }, + // Test case 51. + { + pattern: "my-bucket/mnop*?and", + text: "my-bucket/mnopqanda", + matched: false, + }, + // Test case 52. + + { + pattern: "my-?-bucket/abc*", + text: "my-bucket/mnopqanda", + matched: false, + }, + } + // Iterating over the test cases, call the operation under test and asert the output. + for i, testCase := range testCases { + actualResult := Match(testCase.pattern, testCase.text) + if testCase.matched != actualResult { + t.Errorf("Test %d: Expected the result to be `%v`, but instead found it to be `%v`", i+1, testCase.matched, actualResult) + } + } +} + +func TestKeys(t *testing.T) { + op := &stringLikeOp{} + res := KeySet{} + require.Equal(t, res, op.keys()) +} + +func TestOperator(t *testing.T) { + op := &stringLikeOp{} + require.Equal(t, operator(stringLike), op.operator()) +} diff --git a/objectnode/policy_stringset.go b/objectnode/policy_stringset.go new file mode 100644 index 000000000..6a4f9bd27 --- /dev/null +++ b/objectnode/policy_stringset.go @@ -0,0 +1,166 @@ +// Copyright 2023 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 ( + "encoding/json" + "fmt" + "sort" +) + +type StringSet map[string]struct{} + +// returns StringSet as string slice. +func (set StringSet) ToSlice() []string { + keys := make([]string, 0, len(set)) + for k := range set { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +// returns whether the set is empty or not. +func (set StringSet) IsEmpty() bool { + return len(set) == 0 +} + +// adds string to the set. +func (set StringSet) Add(s string) { + set[s] = struct{}{} +} + +// removes string in the set. It does nothing if string does not exist in the set. +func (set StringSet) Remove(s string) { + delete(set, s) +} + +// checks if string is in the set. +func (set StringSet) Contains(s string) bool { + _, ok := set[s] + return ok +} + +// checks whether given set is equal to current set or not. +func (set StringSet) Equals(sset StringSet) bool { + // If length of set is not equal to length of given set, the + // set is not equal to given set. + if len(set) != len(sset) { + return false + } + + // As both sets are equal in length, check each elements are equal. + for k := range set { + if _, ok := sset[k]; !ok { + return false + } + } + + return true +} + +// returns the intersection with given set as new set. +func (set StringSet) Intersection(sset StringSet) StringSet { + nset := NewStringSet() + for k := range set { + if _, ok := sset[k]; ok { + nset.Add(k) + } + } + + return nset +} + +// returns the difference with given set as new set. +func (set StringSet) Difference(sset StringSet) StringSet { + nset := NewStringSet() + for k := range set { + if _, ok := sset[k]; !ok { + nset.Add(k) + } + } + + return nset +} + +// returns the union with given set as new set. +func (set StringSet) Union(sset StringSet) StringSet { + nset := NewStringSet() + for k := range set { + nset.Add(k) + } + + for k := range sset { + nset.Add(k) + } + + return nset +} + +// converts to JSON data. +func (set StringSet) MarshalJSON() ([]byte, error) { + return json.Marshal(set.ToSlice()) +} + +// parses JSON data and creates new set with it. +// If 'data' contains JSON string array, the set contains each string. +// If 'data' contains JSON string, the set contains the string as one element. +// If 'data' contains Other JSON types, JSON parse error is returned. +func (set *StringSet) UnmarshalJSON(data []byte) error { + sl := []string{} + var err error + if err = json.Unmarshal(data, &sl); err == nil { + *set = make(StringSet) + for _, s := range sl { + set.Add(s) + } + } else { + var s string + if err = json.Unmarshal(data, &s); err == nil { + *set = make(StringSet) + set.Add(s) + } + } + + return err +} + +// returns printable string of the set. +func (set StringSet) String() string { + return fmt.Sprintf("%s", set.ToSlice()) +} + +// creates new string set. +func NewStringSet() StringSet { + return make(StringSet) +} + +// creates new string set with given string values. +func CreateStringSet(sl ...string) StringSet { + set := make(StringSet) + for _, k := range sl { + set.Add(k) + } + return set +} + +// returns copy of given set. +func CopyStringSet(set StringSet) StringSet { + nset := NewStringSet() + for k, v := range set { + nset[k] = v + } + return nset +} diff --git a/objectnode/policy_stringset_test.go b/objectnode/policy_stringset_test.go new file mode 100644 index 000000000..d788b2ff7 --- /dev/null +++ b/objectnode/policy_stringset_test.go @@ -0,0 +1,274 @@ +package objectnode + +import ( + "fmt" + "testing" +) + +func TestNewStringSet(t *testing.T) { + if ss := NewStringSet(); !ss.IsEmpty() { + t.Fatalf("expected: true, got: false") + } +} + +func TestCreateStringSet(t *testing.T) { + ss := CreateStringSet("foo") + if str := ss.String(); str != `[foo]` { + t.Fatalf("expected: %s, got: %s", `["foo"]`, str) + } +} + +func TestCopyStringSet(t *testing.T) { + ss := CreateStringSet("foo") + sscopy := CopyStringSet(ss) + if !ss.Equals(sscopy) { + t.Fatalf("expected: %s, got: %s", ss, sscopy) + } +} + +// StringSet.Add() is called with series of cases for valid and erroneous inputs and the result is validated. +func TestStringSetAdd(t *testing.T) { + testCases := []struct { + value string + expectedResult string + }{ + // Test first addition. + {"foo", `[foo]`}, + // Test duplicate addition. + {"foo", `[foo]`}, + // Test new addition. + {"bar", `[bar foo]`}, + } + + ss := NewStringSet() + for _, testCase := range testCases { + ss.Add(testCase.value) + if str := ss.String(); str != testCase.expectedResult { + t.Fatalf("expected: %s, got: %s", testCase.expectedResult, str) + } + } +} + +func TestStringSetRemove(t *testing.T) { + ss := CreateStringSet("foo", "bar") + testCases := []struct { + value string + expectedResult string + }{ + // Test removing non-existen item. + {"baz", `[bar foo]`}, + // Test remove existing item. + {"foo", `[bar]`}, + // Test remove existing item again. + {"foo", `[bar]`}, + // Test remove to make set to empty. + {"bar", `[]`}, + } + + for _, testCase := range testCases { + ss.Remove(testCase.value) + if str := ss.String(); str != testCase.expectedResult { + t.Fatalf("expected: %s, got: %s", testCase.expectedResult, str) + } + } +} + +func TestStringSetContains(t *testing.T) { + ss := CreateStringSet("foo") + testCases := []struct { + value string + expectedResult bool + }{ + // Test to check non-existent item. + {"bar", false}, + // Test to check existent item. + {"foo", true}, + // Test to verify case sensitivity. + {"Foo", false}, + } + + for _, testCase := range testCases { + if result := ss.Contains(testCase.value); result != testCase.expectedResult { + t.Fatalf("expected: %t, got: %t", testCase.expectedResult, result) + } + } +} + +// StringSet.Equals() is called with series of cases for valid and erroneous inputs and the result is validated. +func TestStringSetEquals(t *testing.T) { + testCases := []struct { + set1 StringSet + set2 StringSet + expectedResult bool + }{ + // Test equal set + {CreateStringSet("foo", "bar"), CreateStringSet("foo", "bar"), true}, + // Test second set with more items + {CreateStringSet("foo", "bar"), CreateStringSet("foo", "bar", "baz"), false}, + // Test second set with less items + {CreateStringSet("foo", "bar"), CreateStringSet("bar"), false}, + } + + for _, testCase := range testCases { + if result := testCase.set1.Equals(testCase.set2); result != testCase.expectedResult { + t.Fatalf("expected: %t, got: %t", testCase.expectedResult, result) + } + } +} + +// StringSet.Intersection() is called with series of cases for valid and erroneous inputs and the result is validated. +func TestStringSetIntersection(t *testing.T) { + testCases := []struct { + set1 StringSet + set2 StringSet + expectedResult StringSet + }{ + // Test intersecting all values. + {CreateStringSet("foo", "bar"), CreateStringSet("foo", "bar"), CreateStringSet("foo", "bar")}, + // Test intersecting all values in second set. + {CreateStringSet("foo", "bar", "baz"), CreateStringSet("foo", "bar"), CreateStringSet("foo", "bar")}, + // Test intersecting different values in second set. + {CreateStringSet("foo", "baz"), CreateStringSet("baz", "bar"), CreateStringSet("baz")}, + // Test intersecting none. + {CreateStringSet("foo", "baz"), CreateStringSet("poo", "bar"), NewStringSet()}, + } + + for _, testCase := range testCases { + if result := testCase.set1.Intersection(testCase.set2); !result.Equals(testCase.expectedResult) { + t.Fatalf("expected: %s, got: %s", testCase.expectedResult, result) + } + } +} + +// StringSet.Difference() is called with series of cases for valid and erroneous inputs and the result is validated. +func TestStringSetDifference(t *testing.T) { + testCases := []struct { + set1 StringSet + set2 StringSet + expectedResult StringSet + }{ + // Test differing none. + {CreateStringSet("foo", "bar"), CreateStringSet("foo", "bar"), NewStringSet()}, + // Test differing in first set. + {CreateStringSet("foo", "bar", "baz"), CreateStringSet("foo", "bar"), CreateStringSet("baz")}, + // Test differing values in both set. + {CreateStringSet("foo", "baz"), CreateStringSet("baz", "bar"), CreateStringSet("foo")}, + // Test differing all values. + {CreateStringSet("foo", "baz"), CreateStringSet("poo", "bar"), CreateStringSet("foo", "baz")}, + } + + for _, testCase := range testCases { + if result := testCase.set1.Difference(testCase.set2); !result.Equals(testCase.expectedResult) { + t.Fatalf("expected: %s, got: %s", testCase.expectedResult, result) + } + } +} + +// StringSet.Union() is called with series of cases for valid and erroneous inputs and the result is validated. +func TestStringSetUnion(t *testing.T) { + testCases := []struct { + set1 StringSet + set2 StringSet + expectedResult StringSet + }{ + // Test union same values. + {CreateStringSet("foo", "bar"), CreateStringSet("foo", "bar"), CreateStringSet("foo", "bar")}, + // Test union same values in second set. + {CreateStringSet("foo", "bar", "baz"), CreateStringSet("foo", "bar"), CreateStringSet("foo", "bar", "baz")}, + // Test union different values in both set. + {CreateStringSet("foo", "baz"), CreateStringSet("baz", "bar"), CreateStringSet("foo", "baz", "bar")}, + // Test union all different values. + {CreateStringSet("foo", "baz"), CreateStringSet("poo", "bar"), CreateStringSet("foo", "baz", "poo", "bar")}, + } + + for _, testCase := range testCases { + if result := testCase.set1.Union(testCase.set2); !result.Equals(testCase.expectedResult) { + t.Fatalf("expected: %s, got: %s", testCase.expectedResult, result) + } + } +} + +// StringSet.MarshalJSON() is called with series of cases for valid and erroneous inputs and the result is validated. +func TestStringSetMarshalJSON(t *testing.T) { + testCases := []struct { + set StringSet + expectedResult string + }{ + // Test set with values. + {CreateStringSet("foo", "bar"), `["bar","foo"]`}, + // Test empty set. + {NewStringSet(), "[]"}, + } + + for _, testCase := range testCases { + if result, _ := testCase.set.MarshalJSON(); string(result) != testCase.expectedResult { + t.Fatalf("expected: %s, got: %s", testCase.expectedResult, string(result)) + } + } +} + +// StringSet.UnmarshalJSON() is called with series of cases for valid and erroneous inputs and the result is validated. +func TestStringSetUnmarshalJSON(t *testing.T) { + testCases := []struct { + data []byte + expectedResult string + }{ + {[]byte(`["bar","foo"]`), `[bar foo]`}, + {[]byte(`"bar"`), `[bar]`}, + {[]byte(`[]`), `[]`}, + {[]byte(`""`), `[]`}, + } + + for _, testCase := range testCases { + var set StringSet + set.UnmarshalJSON(testCase.data) + if result := set.String(); result != testCase.expectedResult { + t.Fatalf("expected: %s, got: %s", testCase.expectedResult, result) + } + } +} + +// StringSet.String() is called with series of cases for valid and erroneous inputs and the result is validated. +func TestStringSetString(t *testing.T) { + testCases := []struct { + set StringSet + expectedResult string + }{ + // Test empty set. + {NewStringSet(), `[]`}, + // Test set with empty value. + {CreateStringSet(""), `[]`}, + // Test set with value. + {CreateStringSet("foo"), `[foo]`}, + } + + for _, testCase := range testCases { + if str := testCase.set.String(); str != testCase.expectedResult { + t.Fatalf("expected: %s, got: %s", testCase.expectedResult, str) + } + } +} + +// StringSet.ToSlice() is called with series of cases for valid and erroneous inputs and the result is validated. +func TestStringSetToSlice(t *testing.T) { + testCases := []struct { + set StringSet + expectedResult string + }{ + // Test empty set. + {NewStringSet(), `[]`}, + // Test set with empty value. + {CreateStringSet(""), `[]`}, + // Test set with value. + {CreateStringSet("foo"), `[foo]`}, + // Test set with value. + {CreateStringSet("foo", "bar"), `[bar foo]`}, + } + + for _, testCase := range testCases { + sslice := testCase.set.ToSlice() + if str := fmt.Sprintf("%s", sslice); str != testCase.expectedResult { + t.Fatalf("expected: %s, got: %s", testCase.expectedResult, str) + } + } +} diff --git a/objectnode/policy_valueset.go b/objectnode/policy_valueset.go new file mode 100644 index 000000000..5764c3bb0 --- /dev/null +++ b/objectnode/policy_valueset.go @@ -0,0 +1,218 @@ +// Copyright 2023 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 ( + "encoding/json" + "fmt" + "reflect" + "strconv" +) + +// is enum type of string, int or bool. +type Value struct { + t reflect.Kind + s string + i int + b bool +} + +// gets stored bool value. +func (v Value) GetBool() (bool, error) { + var err error + + if v.t != reflect.Bool { + err = fmt.Errorf("not a bool Value") + } + + return v.b, err +} + +// gets stored int value. +func (v Value) GetInt() (int, error) { + var err error + + if v.t != reflect.Int { + err = fmt.Errorf("not a int Value") + } + + return v.i, err +} + +// gets stored string value. +func (v Value) GetString() (string, error) { + var err error + + if v.t != reflect.String { + err = fmt.Errorf("not a string Value") + } + + return v.s, err +} + +// gets enum type. +func (v Value) GetType() reflect.Kind { + return v.t +} + +// encodes Value to JSON data. +func (v Value) MarshalJSON() ([]byte, error) { + switch v.t { + case reflect.String: + return json.Marshal(v.s) + case reflect.Int: + return json.Marshal(v.i) + case reflect.Bool: + return json.Marshal(v.b) + } + + return nil, fmt.Errorf("unknown value kind %v", v.t) +} + +// stores bool value. +func (v *Value) StoreBool(b bool) { + *v = Value{t: reflect.Bool, b: b} +} + +// stores int value. +func (v *Value) StoreInt(i int) { + *v = Value{t: reflect.Int, i: i} +} + +// stores string value. +func (v *Value) StoreString(s string) { + *v = Value{t: reflect.String, s: s} +} + +// returns string representation of value. +func (v Value) String() string { + switch v.t { + case reflect.String: + return v.s + case reflect.Int: + return strconv.Itoa(v.i) + case reflect.Bool: + return strconv.FormatBool(v.b) + } + + return "" +} + +// decodes JSON data. +func (v *Value) UnmarshalJSON(data []byte) error { + var b bool + if err := json.Unmarshal(data, &b); err == nil { + v.StoreBool(b) + return nil + } + + var i int + if err := json.Unmarshal(data, &i); err == nil { + v.StoreInt(i) + return nil + } + + var s string + if err := json.Unmarshal(data, &s); err == nil { + v.StoreString(s) + return nil + } + + return fmt.Errorf(unknownJsonData, data) +} + +// returns new bool value. +func NewBoolValue(b bool) Value { + value := &Value{} + value.StoreBool(b) + return *value +} + +// returns new int value. +func NewIntValue(i int) Value { + value := &Value{} + value.StoreInt(i) + return *value +} + +// returns new string value. +func NewStringValue(s string) Value { + value := &Value{} + value.StoreString(s) + return *value +} + +// unique list of values. +type ValueSet map[Value]struct{} + +// adds given value to value set. +func (set ValueSet) Add(value Value) { + set[value] = struct{}{} +} + +// encodes ValueSet to JSON data. +func (set ValueSet) MarshalJSON() ([]byte, error) { + values := []Value{} + for k := range set { + values = append(values, k) + } + + if len(values) == 0 { + return nil, fmt.Errorf("invalid value set %v", set) + } + + return json.Marshal(values) +} + +// decodes JSON data. +func (set *ValueSet) UnmarshalJSON(data []byte) error { + var v Value + if err := json.Unmarshal(data, &v); err == nil { + *set = make(ValueSet) + set.Add(v) + return nil + } + + var values []Value + if err := json.Unmarshal(data, &values); err != nil { + return err + } + + if len(values) < 1 { + return fmt.Errorf(invalidConditionValue) + } + + *set = make(ValueSet) + for _, v = range values { + if _, found := (*set)[v]; found { + return fmt.Errorf(duplicateConditionValue, v) + } + + set.Add(v) + } + + return nil +} + +// returns new value set containing given values. +func NewValueSet(values ...Value) ValueSet { + set := make(ValueSet) + + for _, value := range values { + set.Add(value) + } + + return set +} diff --git a/objectnode/policy_valueset_test.go b/objectnode/policy_valueset_test.go new file mode 100644 index 000000000..7c56dc56e --- /dev/null +++ b/objectnode/policy_valueset_test.go @@ -0,0 +1,172 @@ +package objectnode + +import ( + "encoding/json" + "reflect" + "testing" +) + +func TestValueGetBool(t *testing.T) { + testCases := []struct { + value Value + expectedResult bool + expectErr bool + }{ + {NewBoolValue(true), true, false}, + {NewIntValue(7), false, true}, + {Value{}, false, true}, + } + + for i, testCase := range testCases { + result, err := testCase.value.GetBool() + expectErr := err != nil + + if expectErr != testCase.expectErr { + t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr) + } + + if !testCase.expectErr { + if result != testCase.expectedResult { + t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } + } +} + +func TestValueGetInt(t *testing.T) { + testCases := []struct { + value Value + expectedResult int + expectErr bool + }{ + {NewIntValue(7), 7, false}, + {NewBoolValue(true), 0, true}, + {Value{}, 0, true}, + } + + for i, testCase := range testCases { + result, err := testCase.value.GetInt() + expectErr := err != nil + + if expectErr != testCase.expectErr { + t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr) + } + + if !testCase.expectErr { + if result != testCase.expectedResult { + t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } + } +} + +func TestValueGetString(t *testing.T) { + testCases := []struct { + value Value + expectedResult string + expectErr bool + }{ + {NewStringValue("foo"), "foo", false}, + {NewBoolValue(true), "", true}, + {Value{}, "", true}, + } + + for i, testCase := range testCases { + result, err := testCase.value.GetString() + expectErr := err != nil + + if expectErr != testCase.expectErr { + t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr) + } + + if !testCase.expectErr { + if result != testCase.expectedResult { + t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } + } +} + +func TestValueGetType(t *testing.T) { + testCases := []struct { + value Value + expectedResult reflect.Kind + }{ + {NewBoolValue(true), reflect.Bool}, + {NewIntValue(7), reflect.Int}, + {NewStringValue("foo"), reflect.String}, + {Value{}, reflect.Invalid}, + } + + for i, testCase := range testCases { + result := testCase.value.GetType() + + if result != testCase.expectedResult { + t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) + } + } +} +func TestValueSet_UnmarshalJSON(t *testing.T) { + testCases := []struct { + value []byte + expectedResult ValueSet + expectErr bool + }{ + //test duplicate value + {[]byte(`["www.example.com","foo","foo"]`), NewValueSet(NewStringValue("www.example.com"), NewStringValue("foo"), NewStringValue("foo")), true}, + //test empty value + {[]byte(`[]`), NewValueSet(), true}, + {[]byte(`["www.example.com",["foo"]]`), NewValueSet(NewStringValue("www.example.com")), true}, + //test correct multiple value + {[]byte(`["www.example.com","foo","bar"]`), NewValueSet(NewStringValue("www.example.com"), NewStringValue("foo"), NewStringValue("bar")), false}, + {[]byte(`["www.example.com"]`), NewValueSet(NewStringValue("www.example.com")), false}, + {[]byte(`"www.example.com"`), NewValueSet(NewStringValue("www.example.com")), false}, + {[]byte(`["www.example.com",1,true]`), NewValueSet(NewStringValue("www.example.com"), NewIntValue(1), NewBoolValue(true)), false}, + } + for i, testCase := range testCases { + var result ValueSet + err := json.Unmarshal(testCase.value, &result) + resultErr := (err != nil) + if resultErr != testCase.expectErr { + t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, resultErr) + } + if !testCase.expectErr { + if !reflect.DeepEqual(result, testCase.expectedResult) { + t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result) + } + } + } +} + +func TestValueSet_MarshalJSON(t *testing.T) { + testCases := []struct { + value ValueSet + expectedResult []byte + expectErr bool + }{ + //test duplicate value + {NewValueSet(NewStringValue("www.example.com"), NewStringValue("foo"), NewStringValue("foo")), []byte(`["www.example.com","foo"]`), false}, + //test empty value + {NewValueSet(), []byte(`[]`), true}, + + //test correct multiple value + {NewValueSet(NewStringValue("www.example.com"), NewStringValue("foo"), NewStringValue("bar")), []byte(`["www.example.com","foo","bar"]`), false}, + {NewValueSet(NewStringValue("www.example.com")), []byte(`["www.example.com"]`), false}, + {NewValueSet(NewStringValue("www.example.com"), NewIntValue(1), NewBoolValue(true)), []byte(`["www.example.com",1,true]`), false}, + } + for i, testCase := range testCases { + var result []byte + result, err := json.Marshal(testCase.value) + resultErr := (err != nil) + if resultErr != testCase.expectErr { + t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, resultErr) + } + if !testCase.expectErr { + var r ValueSet + json.Unmarshal(result, &r) + if !reflect.DeepEqual(r, testCase.value) { + t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result)) + } + } + } +} diff --git a/objectnode/result_error.go b/objectnode/result_error.go index f7eb00ef9..759a662a3 100644 --- a/objectnode/result_error.go +++ b/objectnode/result_error.go @@ -26,6 +26,14 @@ type ErrorCode struct { StatusCode int } +func NewError(errCode, errMsg string, statusCode int) ErrorCode { + return ErrorCode{ + ErrorCode: errCode, + ErrorMessage: errMsg, + StatusCode: statusCode, + } +} + func (code ErrorCode) ServeResponse(w http.ResponseWriter, r *http.Request) error { // write status code to request context, // traceMiddleWare send exception request to prometheus via status code @@ -133,3 +141,7 @@ func InternalErrorCode(err error) *ErrorCode { StatusCode: http.StatusInternalServerError, } } + +func (e ErrorCode) Error() string { + return e.ErrorMessage +} diff --git a/objectnode/s3api_name.go b/objectnode/s3api_name.go new file mode 100644 index 000000000..e43c87676 --- /dev/null +++ b/objectnode/s3api_name.go @@ -0,0 +1,94 @@ +// Copyright 2023 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 + +//service api refer to: https://docs.aws.amazon.com/en_pv/AmazonS3/latest/API/archive-RESTServiceOps.html +//bucket api refer to: https://docs.aws.amazon.com/en_pv/AmazonS3/latest/API/archive-RESTBucketOps.html +//object api refer to: https://docs.aws.amazon.com/en_pv/AmazonS3/latest/API/archive-RESTObjectOps +//account api: https://docs.aws.amazon.com/en_pv/AmazonS3/latest/API/archive-RESTAccountOps.html , batch jobs + +const ( + UNSUPPORT_API = "UnSupportAPI" + GET_FEDERATION_TOKEN = "GetFederationToken" //api: POST /, host=s3-cn-east-1.cs.com, create sts token + List_BUCKETS = "ListBuckets" //api: GET / , host=s3-cn-east-1.cs.com, list all buckets + DELETE_BUCKET = "DeleteBucket" //api: Delete / , host=.domain + DELETE_BUCKET_CORS = "DeleteBucketCors" //api: Delete /?cors , host=.domain + DELETE_BUCKET_ENCRYPTION = "DeleteBucketEncryption" //api: Delete /?encryption , host=.domain + DELETE_BUCKET_LIFECYCLE = "DeleteBucketLifeCycle" //api: Delete /?lifycycle , host=.domain + DELETE_BUCKET_METRICS = "DeleteBucketMetrics" //api: Delete /?metrics&id= , host=.domain + DELETE_BUCKET_POLICY = "DeleteBucketPolicy" //api: Delete /?policy , host=.domain + DELETE_BUCKET_REPLICATION = "DeleteBucketReplication" //api: Delete /?replication , host=.domain + DELETE_BUCKET_TAGGING = "DeleteBucketTagging" //api: Delete /?tagging , host=.domain + DELETE_BUCKET_WEBSITE = "DeleteBucketWebsite" //api: Delete /?website , host=.domain + LIST_OBJECTS = "ListObjects" //api: Get / , host=.domain , GetBucket version1 + LIST_OBJECTS_V2 = "ListObjectsV2" //api: Get /?list-type=2, host=.domain, GetBucket Version2 + GET_BUCKET_ACCELERATE = "GetBucketAccelerate" //api: GET /?accelerate + GET_BUCKET_ACL = "GetBucketAcl" //api: GET /?acl + GET_BUCKET_CORS = "GetBucketCors" //api: Get /?cors , host=.domain + GET_BUCKET_ENCRYPTION = "GetBucketEncryption" //api: Get /?encryption , host=.domain + GET_BUCKET_LIFECYCLE = "GetBucketLifeCycle" //api: Get /?lifycycle , host=.domain + GET_BUCKET_LOCATION = "GetBucketLocation" //api: GET /?location , host=.domain + GET_PUBLIC_ACCESS_BLOCK = "GetPublicAccessBlock" //api: Get /?publicAccessBlock , host=.domain + GET_BUCKET_LOGGING = "GetBucketLogging" //api: Get /?logging , host=.domain + GET_BUCKET_METRICS = "GetBucketMetrics" //api: Get /?metrics&id= , host=.domain + GET_BUCKET_NOTIFICATION = "GetBucketNotification" //api: Get /?notification , host=.domain + GET_BUCKET_POLICY_STATUS = "GetBucketPolicyStatus" //api: Get /?policyStatus , host=.domain + GET_BUCKET_OBJECT_VERSIONS = "GetBucketObjectVersions" //api: Get /?versions , host=.domain + GET_BUCKET_POLICY = "GetBucketPolicy" //api: Get /?policy , host=.domain + GET_BUCKET_REPLICATION = "GetBucketReplication" //api: Get /?replication , host=.domain + GET_BUCKET_TAGGING = "GetBucketTagging" //api: Get /?tagging , host=.domain + GET_BUCKET_VERSIONING = "GetBucketVersioning" //api: Get /?versioning , host=.domain + GET_BUCKET_WEBSITE = "GetBucketWebsite" //api: Get /?website , host=.domain + GET_OBJECT_LOCK_CFG = "GetObjectLockConfiguration" //api: Get /?object-lock, host=.domain + HEAD_BUCKET = "HeadBucket" //api: Head / , host=.domain + LIST_MULTIPART_UPLOADS = "ListMultipartUploads" //api: GET /?uploads , host=.domain + PUT_BUCKET = "CreateBucket" //api: Put / , host=.domain, CreateBucket + PUT_BUCKET_ACCELERATE = "PutBucketAccelerate" //api: Put /?accelerate , host=.domain, + PUT_BUCKET_ACL = "PutBucketAcl" //api: Put /?acl , host=.domain + PUT_BUCKET_CORS = "PutBucketCors" //api: PUT /?cors , host=.domain + PUT_BUCKET_ENCRYPTION = "PutBucketEncryption" //api: PUT /?encryption , host=.domain + PUT_BUCKET_LIFECYCLE = "PutBucketLifecycle" //api: PUT /?lifecycle , host=.domain + PUT_PUBLIC_ACCESS_BLOCK = "PutPublicAccessBlock" //api: PUT /?publicAccessBlock , host=.domain, + PUT_BUCKET_LOGGING = "PutBucketLogging" //api: PUT /?logging , host=.domain + PUT_BUCKET_METRICS = "PutBucketMetrics" //api: PUT /?metrics&id= , host=.domain + PUT_BUCKET_NOTIFICATION = "PutBucketNotification" //api: PUT /?notification , host=.domain + PUT_BUCKET_POLICY = "PutBucketPolicy" //api: PUT /?policy , host=.domain + PUT_BUCKET_REPLICATION = "PutBucketReplication" //api: PUT /?replication , host=.domain + PUT_BUCKET_REQUEST_PAYMENT = "PutBucketRequestPayment" //api: PUT /?requestPayment , host=.domain + PUT_BUCKET_TAGGING = "PutBucketTagging" //api: PUT /?tagging , host=.domain + PUT_BUCKET_VERSIONING = "PutBucketVersioning" //api: PUT /?versioning , host=.domain + PUT_BUCKET_WEBSITE = "PutBucketWebsite" //api: PUT /?website , host=.domain + PUT_OBJECT_LOCK_CFG = "PutObjectLockConfiguration" //api: Put /?object-lock, host=.domain + BATCH_DELETE = "DeleteObjects" //api: POST /?delete , host=.domain, "DeleteObjects" + DELETE_OBJECT = "DeleteObject" //api: Delete /, host=.domain + DELETE_OBJECT_TAGGING = "DeleteObjectTagging" //api: Delete /?tagging, host=.domain + GET_OBJECT = "GetObject" //api: Get / , host=.domain + GET_OBJECT_ACL = "GetObjectAcl" //api: Get //?acl , host=.domain + GET_OBJECT_TAGGING = "GetObjectTagging" //api: Get //?tagging , host=.domain + GET_OBJECT_RETENTION = "GetObjectRetention" //api: Get //?retention, host=.domain + HEAD_OBJECT = "HeadObject" //api: HEAD / , host=.domain + OPTIONS_OBJECT = "OptionsObject" //api: OPTIONS /, host=.domain + POST_OBJECT = "PostObject" //api: Post / , host=.domain + PUT_OBJECT = "PutObject" //api: Put /, host=.domain + COPY_OBJECT = "CopyObject" //api: Put / ,host=.domain, header["x-amz-copy-source"] + PUT_OBJECT_ACL = "PutObjectAcl" //api: Put /?acl , host=.domain + PUT_OBJECT_TAGGING = "PutObjectTagging" //api: Put /?tagging , host=.domain + INITIALE_MULTIPART_UPLOAD = "CreateMultipartUpload" //api: POST /?uploads , host=.domain + UPLOAD_PART = "UploadPart" //api: PUT /?partNumber=&uploadId=, host=.domain + UPLOAD_PART_COPY = "UploadPartCopy" //api: PUT /?partNumber=&uploadId=, host=.domain , header["x-amz-copy-source"] + LIST_PARTS = "ListParts" //api: GET /?uploadId= , host=.domain + COMPLETE_MULTIPART_UPLOAD = "CompleteMultipartUpload" //api: POST /?uploadId= , host=.domain + ABORT_MULTIPART_UPLOAD = "AbortMultipartUpload" //api: DELETE /?uploadId= , host=.domain +) diff --git a/objectnode/stringset.go b/objectnode/stringset.go deleted file mode 100644 index 2b904bbbc..000000000 --- a/objectnode/stringset.go +++ /dev/null @@ -1,138 +0,0 @@ -/* - * MinIO Cloud Storage, (C) 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 objectnode - -// https://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html - -import ( - "encoding/json" - "strings" -) - -//https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/example-bucket-policies.html - -type null struct{} - -var ( - void null = struct{}{} -) - -type StringSet struct { - values map[string]null -} - -func (ss StringSet) String() string { - s := make([]string, 0, len(ss.values)) - for k := range ss.values { - s = append(s, k) - } - return "[" + strings.Join(s, ",") + "]" -} - -func (ss StringSet) MarshalJSON() ([]byte, error) { - if len(ss.values) == 0 { - return json.Marshal(nil) - } - array := make([]string, 0) - for v := range ss.values { - array = append(array, v) - } - return json.Marshal(array) -} - -func (ss *StringSet) UnmarshalJSON(b []byte) error { - ss.values = make(map[string]null) - var s string - if err := json.Unmarshal(b, &s); err == nil { - ss.values[s] = void - return nil - } - var sl []string - if err := json.Unmarshal(b, &sl); err == nil { - for _, s := range sl { - ss.values[s] = void - } - return nil - } - - var il []interface{} - if err := json.Unmarshal(b, &il); err == nil { - for _, i := range il { - ss.values[i.(string)] = void - } - return nil - } - - return nil -} - -func (ss *StringSet) Empty() bool { - return len(ss.values) == 0 -} - -func (ss *StringSet) ContainsWithAny(val string) bool { - strs := strings.Split(val, ":") - if len(strs) > 1 { - prefix := strs[0] - any := prefix + ":*" - if _, ok1 := ss.values[any]; ok1 { - return true - } - } - return ss.Contains(val) -} - -func (ss *StringSet) Contains(val string) bool { - _, ok := ss.values[val] - return ok -} - -func (ss *StringSet) ContainsRegex(val string) bool { - if ss.Contains("*") { - return true - } - if ss.Contains(val) { - return true - } - for k := range ss.values { - if patternMatch(k, val) { - return true - } - } - return false -} - -func (ss *StringSet) ContainsWild(val string) bool { - if ss.Contains("*") { - return true - } - if ss.Contains(val) { - return true - } - - return false -} - -func (ss *StringSet) Intersection(set *StringSet) bool { - for s := range ss.values { - if set.ContainsWild(s) { - return true - } - } - - return false -}