mirror of
https://github.com/cubefs/cubefs.git
synced 2026-08-02 02:00:56 +00:00
feature: bucket policy fully compatible with s3
Signed-off-by: tangdeyi <tangdeyi@oppo.com>
This commit is contained in:
parent
108d7af81c
commit
9f0d7810a0
@ -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
|
||||
}
|
||||
|
||||
@ -20,10 +20,9 @@ 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 {
|
||||
@ -31,8 +30,8 @@ type RequestParam struct {
|
||||
bucket string
|
||||
object string
|
||||
action proto.Action
|
||||
apiName string
|
||||
sourceIP string
|
||||
conditionVars map[string][]string
|
||||
vars map[string]string
|
||||
accessKey string
|
||||
r *http.Request
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
// 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
|
||||
}
|
||||
// Check user policy
|
||||
allowed = false
|
||||
return
|
||||
}
|
||||
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,16 +241,80 @@ 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) {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
//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() {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
//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("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("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
|
||||
}
|
||||
@ -286,34 +324,59 @@ func (o *ObjectNode) policyCheck(f http.HandlerFunc) http.HandlerFunc {
|
||||
if policy, err = vol.metaLoader.loadPolicy(); err != nil {
|
||||
return
|
||||
}
|
||||
if vv, err = o.mc.ClientAPI().GetVolumeWithoutAuthKey(bucket); err != nil {
|
||||
return
|
||||
}
|
||||
if err = loadBucketMeta(param.Bucket()); err != nil {
|
||||
log.LogErrorf("policyCheck: load bucket metadata fail: requestID(%v) err(%v)", GetRequestID(r), err)
|
||||
allowed = false
|
||||
ec = NoSuchBucket
|
||||
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() {
|
||||
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())
|
||||
return
|
||||
conditionCheck := map[string]string{
|
||||
SOURCEIP: param.sourceIP,
|
||||
KEYNAME: srcKey,
|
||||
REFERER: param.r.Referer(),
|
||||
HOST: param.r.Host,
|
||||
}
|
||||
}
|
||||
if vol != nil && 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())
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
allowed = true
|
||||
log.LogDebugf("policyCheck: action allowed: requestID(%v) userID(%v) accessKey(%v) volume(%v) action(%v)",
|
||||
GetRequestID(r), userInfo, param.AccessKey(), param.Bucket(), param.Action())
|
||||
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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
func (op operator) IsValid() bool {
|
||||
for _, supOp := range supportedOperators {
|
||||
if op == supOp {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return key
|
||||
return false
|
||||
}
|
||||
|
||||
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},
|
||||
// encodes operator to JSON data.
|
||||
func (op operator) MarshalJSON() ([]byte, error) {
|
||||
if !op.IsValid() {
|
||||
return nil, fmt.Errorf("invalid operator %v", op)
|
||||
}
|
||||
|
||||
for k, v := range r.Header {
|
||||
if existsV, ok := values[k]; ok {
|
||||
values[k] = append(existsV, v...)
|
||||
return json.Marshal(string(op))
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// returns list of keys used in all operation.
|
||||
func (operations Condition) Keys() KeySet {
|
||||
keySet := NewKeySet()
|
||||
|
||||
for _, op := range operations {
|
||||
keySet.AddAll(op.keys())
|
||||
}
|
||||
|
||||
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 {
|
||||
values[k] = v
|
||||
conditionKV[op.operator()] = op.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range r.URL.Query() {
|
||||
if existsV, ok := values[k]; ok {
|
||||
values[k] = append(existsV, v...)
|
||||
return json.Marshal(conditionKV)
|
||||
}
|
||||
|
||||
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 err
|
||||
}
|
||||
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 nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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 err
|
||||
}
|
||||
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 {
|
||||
values[k] = v
|
||||
return fmt.Errorf(cannotTransformToString, value)
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
||||
return values
|
||||
return fmt.Errorf(cannotHandledConditionValue, values)
|
||||
}
|
||||
m[key] = valueSet
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
for k, v := range s.Condition {
|
||||
f, ok := ConditionFuncMap[k]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !f(param, v) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func BoolFunc(p *RequestParam, policyCondtion ConditionValues) bool {
|
||||
if len(policyCondtion) == 0 {
|
||||
return true
|
||||
}
|
||||
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 false
|
||||
}
|
||||
|
||||
func DateEqualsFunc(p *RequestParam, policyVals ConditionValues) bool {
|
||||
for k, pVals := range policyVals {
|
||||
for pVal := range pVals.values {
|
||||
pDate, err := time.Parse(AMZTimeFormat, pVal)
|
||||
op, err := vfn(m)
|
||||
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 pDate.Equal(reqDate)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
ops = append(ops, op)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func DateNotEqualsFunc(p *RequestParam, value ConditionValues) bool {
|
||||
return !DateEqualsFunc(p, value)
|
||||
}
|
||||
|
||||
func DateLessThanFunc(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
261
objectnode/policy_condition_test.go
Normal file
261
objectnode/policy_condition_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
136
objectnode/policy_const.go
Normal file
136
objectnode/policy_const.go
Normal file
@ -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
|
||||
}
|
||||
184
objectnode/policy_ipaddressop.go
Normal file
184
objectnode/policy_ipaddressop.go
Normal file
@ -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
|
||||
}
|
||||
39
objectnode/policy_ipaddressop_test.go
Normal file
39
objectnode/policy_ipaddressop_test.go
Normal file
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
175
objectnode/policy_keyset.go
Normal file
175
objectnode/policy_keyset.go
Normal file
@ -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
|
||||
}
|
||||
98
objectnode/policy_keyset_test.go
Normal file
98
objectnode/policy_keyset_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
250
objectnode/policy_match.go
Normal file
250
objectnode/policy_match.go
Normal file
@ -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)
|
||||
|
||||
}
|
||||
325
objectnode/policy_match_test.go
Normal file
325
objectnode/policy_match_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
@ -17,101 +17,298 @@
|
||||
|
||||
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"
|
||||
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"`
|
||||
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) {
|
||||
return s.isValid(bucket)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
for _, f := range checkFuncs {
|
||||
checked := f(p)
|
||||
if !checked {
|
||||
return false
|
||||
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 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
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s Statement) checkResources(p *RequestParam) bool {
|
||||
if s.Resources.Empty() {
|
||||
return true
|
||||
}
|
||||
if s.Resources.ContainsRegex(p.resource) {
|
||||
func (s *Statement) isEffectValid() bool {
|
||||
e := strings.ToLower(s.Effect)
|
||||
if e == "allow" || e == "deny" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s Statement) checkNotResources(p *RequestParam) bool {
|
||||
if s.NotResources.Empty() {
|
||||
return true
|
||||
// "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
|
||||
}
|
||||
if s.NotResources.ContainsRegex(p.resource) {
|
||||
case map[string]interface{}:
|
||||
p := s.Principal.(map[string]interface{})
|
||||
if !PrincipalType(p).valid() {
|
||||
return false
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
311
objectnode/policy_statement_test.go
Normal file
311
objectnode/policy_statement_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
201
objectnode/policy_stringlikeop.go
Normal file
201
objectnode/policy_stringlikeop.go
Normal file
@ -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
|
||||
}
|
||||
457
objectnode/policy_stringlikeop_test.go
Normal file
457
objectnode/policy_stringlikeop_test.go
Normal file
@ -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())
|
||||
}
|
||||
166
objectnode/policy_stringset.go
Normal file
166
objectnode/policy_stringset.go
Normal file
@ -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
|
||||
}
|
||||
274
objectnode/policy_stringset_test.go
Normal file
274
objectnode/policy_stringset_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
218
objectnode/policy_valueset.go
Normal file
218
objectnode/policy_valueset.go
Normal file
@ -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
|
||||
}
|
||||
172
objectnode/policy_valueset_test.go
Normal file
172
objectnode/policy_valueset_test.go
Normal file
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
94
objectnode/s3api_name.go
Normal file
94
objectnode/s3api_name.go
Normal file
@ -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=<bucket>.domain
|
||||
DELETE_BUCKET_CORS = "DeleteBucketCors" //api: Delete /?cors , host=<bucket>.domain
|
||||
DELETE_BUCKET_ENCRYPTION = "DeleteBucketEncryption" //api: Delete /?encryption , host=<bucket>.domain
|
||||
DELETE_BUCKET_LIFECYCLE = "DeleteBucketLifeCycle" //api: Delete /?lifycycle , host=<bucket>.domain
|
||||
DELETE_BUCKET_METRICS = "DeleteBucketMetrics" //api: Delete /?metrics&id=<ID> , host=<bucket>.domain
|
||||
DELETE_BUCKET_POLICY = "DeleteBucketPolicy" //api: Delete /?policy , host=<bucket>.domain
|
||||
DELETE_BUCKET_REPLICATION = "DeleteBucketReplication" //api: Delete /?replication , host=<bucket>.domain
|
||||
DELETE_BUCKET_TAGGING = "DeleteBucketTagging" //api: Delete /?tagging , host=<bucket>.domain
|
||||
DELETE_BUCKET_WEBSITE = "DeleteBucketWebsite" //api: Delete /?website , host=<bucket>.domain
|
||||
LIST_OBJECTS = "ListObjects" //api: Get / , host=<bucket>.domain , GetBucket version1
|
||||
LIST_OBJECTS_V2 = "ListObjectsV2" //api: Get /?list-type=2, host=<bucket>.domain, GetBucket Version2
|
||||
GET_BUCKET_ACCELERATE = "GetBucketAccelerate" //api: GET /<bucketname>?accelerate
|
||||
GET_BUCKET_ACL = "GetBucketAcl" //api: GET /<bucketname>?acl
|
||||
GET_BUCKET_CORS = "GetBucketCors" //api: Get /?cors , host=<bucket>.domain
|
||||
GET_BUCKET_ENCRYPTION = "GetBucketEncryption" //api: Get /?encryption , host=<bucket>.domain
|
||||
GET_BUCKET_LIFECYCLE = "GetBucketLifeCycle" //api: Get /?lifycycle , host=<bucket>.domain
|
||||
GET_BUCKET_LOCATION = "GetBucketLocation" //api: GET /?location , host=<bucket>.domain
|
||||
GET_PUBLIC_ACCESS_BLOCK = "GetPublicAccessBlock" //api: Get /?publicAccessBlock , host=<bucket>.domain
|
||||
GET_BUCKET_LOGGING = "GetBucketLogging" //api: Get /?logging , host=<bucket>.domain
|
||||
GET_BUCKET_METRICS = "GetBucketMetrics" //api: Get /?metrics&id=<id> , host=<bucket>.domain
|
||||
GET_BUCKET_NOTIFICATION = "GetBucketNotification" //api: Get /?notification , host=<bucket>.domain
|
||||
GET_BUCKET_POLICY_STATUS = "GetBucketPolicyStatus" //api: Get /?policyStatus , host=<bucket>.domain
|
||||
GET_BUCKET_OBJECT_VERSIONS = "GetBucketObjectVersions" //api: Get /?versions , host=<bucket>.domain
|
||||
GET_BUCKET_POLICY = "GetBucketPolicy" //api: Get /?policy , host=<bucket>.domain
|
||||
GET_BUCKET_REPLICATION = "GetBucketReplication" //api: Get /?replication , host=<bucket>.domain
|
||||
GET_BUCKET_TAGGING = "GetBucketTagging" //api: Get /?tagging , host=<bucket>.domain
|
||||
GET_BUCKET_VERSIONING = "GetBucketVersioning" //api: Get /?versioning , host=<bucket>.domain
|
||||
GET_BUCKET_WEBSITE = "GetBucketWebsite" //api: Get /?website , host=<bucket>.domain
|
||||
GET_OBJECT_LOCK_CFG = "GetObjectLockConfiguration" //api: Get /?object-lock, host=<bucket>.domain
|
||||
HEAD_BUCKET = "HeadBucket" //api: Head / , host=<bucket>.domain
|
||||
LIST_MULTIPART_UPLOADS = "ListMultipartUploads" //api: GET /?uploads , host=<bucket>.domain
|
||||
PUT_BUCKET = "CreateBucket" //api: Put / , host=<bucket>.domain, CreateBucket
|
||||
PUT_BUCKET_ACCELERATE = "PutBucketAccelerate" //api: Put /?accelerate , host=<bucket>.domain,
|
||||
PUT_BUCKET_ACL = "PutBucketAcl" //api: Put /?acl , host=<bucket>.domain
|
||||
PUT_BUCKET_CORS = "PutBucketCors" //api: PUT /?cors , host=<bucket>.domain
|
||||
PUT_BUCKET_ENCRYPTION = "PutBucketEncryption" //api: PUT /?encryption , host=<bucket>.domain
|
||||
PUT_BUCKET_LIFECYCLE = "PutBucketLifecycle" //api: PUT /?lifecycle , host=<bucket>.domain
|
||||
PUT_PUBLIC_ACCESS_BLOCK = "PutPublicAccessBlock" //api: PUT /<bucketname>?publicAccessBlock , host=<bucket>.domain,
|
||||
PUT_BUCKET_LOGGING = "PutBucketLogging" //api: PUT /?logging , host=<bucket>.domain
|
||||
PUT_BUCKET_METRICS = "PutBucketMetrics" //api: PUT /?metrics&id=<id> , host=<bucket>.domain
|
||||
PUT_BUCKET_NOTIFICATION = "PutBucketNotification" //api: PUT /?notification , host=<bucket>.domain
|
||||
PUT_BUCKET_POLICY = "PutBucketPolicy" //api: PUT /?policy , host=<bucket>.domain
|
||||
PUT_BUCKET_REPLICATION = "PutBucketReplication" //api: PUT /?replication , host=<bucket>.domain
|
||||
PUT_BUCKET_REQUEST_PAYMENT = "PutBucketRequestPayment" //api: PUT /?requestPayment , host=<bucket>.domain
|
||||
PUT_BUCKET_TAGGING = "PutBucketTagging" //api: PUT /?tagging , host=<bucket>.domain
|
||||
PUT_BUCKET_VERSIONING = "PutBucketVersioning" //api: PUT /?versioning , host=<bucket>.domain
|
||||
PUT_BUCKET_WEBSITE = "PutBucketWebsite" //api: PUT /?website , host=<bucket>.domain
|
||||
PUT_OBJECT_LOCK_CFG = "PutObjectLockConfiguration" //api: Put /?object-lock, host=<bucket>.domain
|
||||
BATCH_DELETE = "DeleteObjects" //api: POST /?delete , host=<bucket>.domain, "DeleteObjects"
|
||||
DELETE_OBJECT = "DeleteObject" //api: Delete /<objname>, host=<bucket>.domain
|
||||
DELETE_OBJECT_TAGGING = "DeleteObjectTagging" //api: Delete /<objname>?tagging, host=<bucket>.domain
|
||||
GET_OBJECT = "GetObject" //api: Get /<objname> , host=<bucket>.domain
|
||||
GET_OBJECT_ACL = "GetObjectAcl" //api: Get /<bucketname>/<objname>?acl , host=<bucket>.domain
|
||||
GET_OBJECT_TAGGING = "GetObjectTagging" //api: Get /<bucketname>/<objname>?tagging , host=<bucket>.domain
|
||||
GET_OBJECT_RETENTION = "GetObjectRetention" //api: Get /<bucketname>/<objname>?retention, host=<bucket>.domain
|
||||
HEAD_OBJECT = "HeadObject" //api: HEAD /<ObjectName> , host=<bucket>.domain
|
||||
OPTIONS_OBJECT = "OptionsObject" //api: OPTIONS /<ObjectName>, host=<bucket>.domain
|
||||
POST_OBJECT = "PostObject" //api: Post / , host=<bucket>.domain
|
||||
PUT_OBJECT = "PutObject" //api: Put /<objname>, host=<bucket>.domain
|
||||
COPY_OBJECT = "CopyObject" //api: Put /<destObjname> ,host=<destbucket>.domain, header["x-amz-copy-source"]
|
||||
PUT_OBJECT_ACL = "PutObjectAcl" //api: Put /<ObjectName>?acl , host=<bucket>.domain
|
||||
PUT_OBJECT_TAGGING = "PutObjectTagging" //api: Put /<ObjectName>?tagging , host=<bucket>.domain
|
||||
INITIALE_MULTIPART_UPLOAD = "CreateMultipartUpload" //api: POST /<ObjectName>?uploads , host=<bucket>.domain
|
||||
UPLOAD_PART = "UploadPart" //api: PUT /<ObjectName>?partNumber=<PartNumber>&uploadId=<Id>, host=<bucket>.domain
|
||||
UPLOAD_PART_COPY = "UploadPartCopy" //api: PUT /<ObjectName>?partNumber=<PartNumber>&uploadId=<Id>, host=<bucket>.domain , header["x-amz-copy-source"]
|
||||
LIST_PARTS = "ListParts" //api: GET /<ObjectName>?uploadId=<Id> , host=<bucket>.domain
|
||||
COMPLETE_MULTIPART_UPLOAD = "CompleteMultipartUpload" //api: POST /<ObjectName>?uploadId=<Id> , host=<bucket>.domain
|
||||
ABORT_MULTIPART_UPLOAD = "AbortMultipartUpload" //api: DELETE /<ObjectName>?uploadId=<Id> , host=<bucket>.domain
|
||||
)
|
||||
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user