feature: bucket policy fully compatible with s3

Signed-off-by: tangdeyi <tangdeyi@oppo.com>
This commit is contained in:
tangdeyi 2023-03-07 23:22:23 -08:00 committed by leonrayang
parent 108d7af81c
commit 9f0d7810a0
27 changed files with 4095 additions and 741 deletions

View File

@ -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
}

View File

@ -20,22 +20,21 @@ import (
"strings"
"github.com/cubefs/cubefs/proto"
"github.com/cubefs/cubefs/util/log"
"github.com/gorilla/mux"
"github.com/cubefs/cubefs/util/log"
)
type RequestParam struct {
resource string
bucket string
object string
action proto.Action
sourceIP string
conditionVars map[string][]string
vars map[string]string
accessKey string
r *http.Request
resource string
bucket string
object string
action proto.Action
apiName string
sourceIP string
vars map[string]string
accessKey string
r *http.Request
}
func (p *RequestParam) Bucket() string {
@ -57,10 +56,6 @@ func (p *RequestParam) GetVar(name string) string {
return p.r.FormValue(name)
}
func (p *RequestParam) GetConditionVar(name string) []string {
return p.conditionVars[name]
}
func (p *RequestParam) AccessKey() string {
return p.accessKey
}
@ -72,7 +67,6 @@ func ParseRequestParam(r *http.Request) *RequestParam {
p.bucket = p.vars["bucket"]
p.object = p.vars["object"]
p.sourceIP = getRequestIP(r)
p.conditionVars = getCondtionValues(r)
if len(p.bucket) > 0 {
p.resource = p.bucket
if len(p.object) > 0 {
@ -91,6 +85,7 @@ func ParseRequestParam(r *http.Request) *RequestParam {
if p.action.IsNone() {
p.action = ActionFromRouteName(mux.CurrentRoute(r).GetName())
}
p.apiName = strings.TrimPrefix(string(p.action), proto.OSSActionPrefix)
return p
}

View File

@ -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

View File

@ -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
}

View File

@ -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)

View File

@ -22,23 +22,34 @@ package objectnode
import (
"encoding/json"
"errors"
"io"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/cubefs/cubefs/proto"
"github.com/cubefs/cubefs/util/log"
)
type ActionType string
"github.com/gorilla/mux"
)
// https://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html
const (
PolicyDefaultVersion = "2012-10-17"
BucketPolicyLimitSize = 20 * 1024 //Bucket policies are limited to 20KB
ArnSplitToken = ":"
maxStatementNum = 10
)
var (
ErrMissingVersionInPolicy = &ErrorCode{ErrorCode: "ErrMissingVersionInPolicy", ErrorMessage: "missing Version in policy", StatusCode: http.StatusBadRequest}
ErrMissingStatementInPolicy = &ErrorCode{ErrorCode: "MissingStatementInPolicy", ErrorMessage: "missing Statement in policy", StatusCode: http.StatusBadRequest}
ErrMissingEffectInPolicy = &ErrorCode{ErrorCode: "MissingEffectInPolicy", ErrorMessage: "missing Effect in policy", StatusCode: http.StatusBadRequest}
ErrMissingPrincipalInPolicy = &ErrorCode{ErrorCode: "MissingPrincipalInPolicy", ErrorMessage: "missing Principal in policy", StatusCode: http.StatusBadRequest}
ErrMissingActionInPolicy = &ErrorCode{ErrorCode: "MissingActionInPolicy", ErrorMessage: "missing Action in policy", StatusCode: http.StatusBadRequest}
ErrMissingResourceInPolicy = &ErrorCode{ErrorCode: "MissingResourceInPolicy", ErrorMessage: "missing Resource in policy", StatusCode: http.StatusBadRequest}
ErrTooManyStatementInPolicy = &ErrorCode{ErrorCode: "TooManyStatementInPolicy", ErrorMessage: "too many statement in policy", StatusCode: http.StatusBadRequest}
ErrInvalidEffectValue = &ErrorCode{ErrorCode: "InvalidEffectValue", ErrorMessage: "Effect can only be Allow or Deny", StatusCode: http.StatusBadRequest}
ErrInvalidPricipalInPolicy = &ErrorCode{ErrorCode: "InvalidPricipalInPolicy", ErrorMessage: "Invalid Principal in policy", StatusCode: http.StatusBadRequest}
ErrInvalidActionInPolicy = &ErrorCode{ErrorCode: "InvalidActionInPolicy", ErrorMessage: "Invalid Action in policy", StatusCode: http.StatusBadRequest}
ErrInvalidResourceInPolicy = &ErrorCode{ErrorCode: "InvalidResourceInPolicy", ErrorMessage: "Invalid Resource in policy", StatusCode: http.StatusBadRequest}
ErrInvalidActionResourceCombination = &ErrorCode{ErrorCode: "InvalidActionResourceCombination", ErrorMessage: "Action does not apply to any resource in statement", StatusCode: http.StatusBadRequest}
)
//https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/example-bucket-policies.html
@ -53,42 +64,6 @@ func (p *Policy) IsEmpty() bool {
return len(p.Statements) == 0
}
// arn:partition:service:region:account-id:resource-id
// arn:partition:service:region:account-id:resource-type/resource-id
// arn:partition:service:region:account-id:resource-type:resource-id
type Arn struct {
arn Resource
partition string //aws
service string //s3/iam
region string //
accountId string //
resourceType string //
resourceId string //
}
func parseArn(str string) (*Arn, error) {
items := strings.Split(str, ArnSplitToken)
if len(items) < 4 {
log.LogErrorf("Arn is invalid: %v", str)
return nil, errors.New("invalid arn")
}
arn := &Arn{
partition: items[1],
service: items[2],
region: items[3],
accountId: items[4],
}
if len(items) > 6 {
arn.resourceType = items[5]
arn.resourceId = items[6]
} else {
arn.resourceId = items[5]
}
return arn, nil
}
// write bucket policy into store and update vol policy meta
func storeBucketPolicy(bytes []byte, vol *Volume) (*Policy, error) {
policy := &Policy{}
@ -126,30 +101,21 @@ func deleteBucketPolicy(vol *Volume) (err error) {
return nil
}
func ParsePolicy(r io.Reader, bucket string) (*Policy, error) {
var policy Policy
d := json.NewDecoder(r)
d.DisallowUnknownFields()
if err := d.Decode(&policy); err != nil {
return nil, err
}
if ok, err := policy.Validate(bucket); !ok {
return nil, err
}
return &policy, nil
}
func (p Policy) isValid() (bool, error) {
if p.Version == "" {
return false, errors.New("policy version cannot be empty")
return false, ErrMissingVersionInPolicy
}
if len(p.Statements) == 0 {
return false, ErrMissingStatementInPolicy
}
if len(p.Statements) > maxStatementNum {
return false, ErrTooManyStatementInPolicy
}
return true, nil
}
func (p Policy) Validate(bucket string) (bool, error) {
log.LogDebug("check policy syntax")
if ok, err1 := p.isValid(); !ok {
return false, err1
}
@ -165,33 +131,29 @@ func (p Policy) Validate(bucket string) (bool, error) {
// check policy is allowed for request
// https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/reference_policies_evaluation-logic.html
func (p *Policy) IsAllowed(params *RequestParam, isOwner bool) bool {
for _, s := range p.Statements {
if s.Effect == Deny {
if !s.IsAllowed(params) {
log.LogDebugf("policy deny cause of %v, %v", s, params)
return false
}
func (p *Policy) IsAllowed(params *RequestParam, reqUid, ownerUid string, conditionCheck map[string]string) PolicyCheckResult {
result := POLICY_UNKNOW
apiName := params.apiName
// only bucket owner is allowed to put/get/delete bucket policy
if isPolicyApi(apiName) {
if reqUid == ownerUid {
return POLICY_ALLOW
}
return POLICY_DENY
}
if !supportByPolicy(apiName) {
return POLICY_UNKNOW
}
for _, statement := range p.Statements {
if tmp := statement.CheckPolicy(apiName, reqUid, conditionCheck); tmp == POLICY_DENY {
log.LogDebugf("bucket policy check: statement denied: requestID(%v) statement(%v)", GetRequestID(params.r), statement)
return POLICY_DENY
} else if tmp == POLICY_ALLOW {
log.LogDebugf("bucket policy check: statement allowed: requestID(%v) statement(%v)", GetRequestID(params.r), statement)
result = POLICY_ALLOW
}
}
//is owner
if isOwner {
return true
}
for _, s := range p.Statements {
if s.Effect == Allow {
if s.IsAllowed(params) {
log.LogDebugf("policy allow cause of %v, %v", s, params)
return true
}
}
}
log.LogDebugf("policy deny cause of %v, request: %v", p, params)
return false
return result
}
func (o *ObjectNode) policyCheck(f http.HandlerFunc) http.HandlerFunc {
@ -213,19 +175,21 @@ func (o *ObjectNode) policyCheck(f http.HandlerFunc) http.HandlerFunc {
}()
param := ParseRequestParam(r)
if param.Bucket() == "" {
log.LogDebugf("policyCheck: no bucket specified: requestID(%v)", GetRequestID(r))
allowed = true
return
}
// A create bucket action do not need to check any user policy and volume policy.
if param.action == proto.OSSCreateBucketAction {
allowed = true
// step1. The account level api does not need to check any user policy and volume policy.
if IsAccountLevelApi(param.apiName) {
if !isAnonymous(param.accessKey) {
allowed = true
return
}
allowed = false
return
}
// Check user policy
var volume *Volume
if bucket := mux.Vars(r)["bucket"]; len(bucket) > 0 {
if volume, err = o.getVol(bucket); err != nil {
@ -238,12 +202,23 @@ func (o *ObjectNode) policyCheck(f http.HandlerFunc) http.HandlerFunc {
return
}
}
var userInfo *proto.UserInfo
//step2. Check user policy
userInfo := new(proto.UserInfo)
isOwner := false
if isAnonymous(param.accessKey) && apiAllowAnonymous(param.apiName) {
log.LogDebugf("anonymous user: requestID(%v)", GetRequestID(r))
goto policycheck
}
if isAnonymous(param.accessKey) && !apiAllowAnonymous(param.apiName) {
log.LogDebugf("anonymous user is not allowed by api(%v) requestID(%v)", param.apiName, GetRequestID(r))
allowed = false
return
}
if userInfo, err = o.getUserInfoByAccessKey(param.AccessKey()); err == nil {
// White list for admin and root user.
if userInfo.UserType == proto.UserTypeRoot || userInfo.UserType == proto.UserTypeAdmin {
log.LogDebugf("policyCheck: user is admin: requestID(%v) userID(%v) accessKey(%v) volume(%v)",
log.LogDebugf("user policy check: user is admin: requestID(%v) userID(%v) accessKey(%v) volume(%v)",
GetRequestID(r), userInfo.UserID, param.AccessKey(), param.Bucket())
allowed = true
return
@ -254,11 +229,10 @@ func (o *ObjectNode) policyCheck(f http.HandlerFunc) http.HandlerFunc {
if subdir == "" {
subdir = r.URL.Query().Get(ParamPrefix)
}
// The bucket is not owned by request user who has not been authorized, so bucket policy should be checked.
if !isOwner && !userPolicy.IsAuthorized(param.Bucket(), subdir, param.Action()) {
log.LogDebugf("policyCheck: user no permission: url(%v) subdir(%v) requestID(%v) userID(%v) accessKey(%v) volume(%v) object(%v) action(%v)",
r.URL, subdir, GetRequestID(r), userInfo.UserID, param.AccessKey(), param.Bucket(), param.Object(), param.Action())
allowed = false
return
log.LogDebugf("user policy check: permission unknown url(%v) subdir(%v) requestID(%v) userID(%v) accessKey(%v) volume(%v) object(%v) action(%v) authorizedVols(%v)",
r.URL, subdir, GetRequestID(r), userInfo.UserID, param.AccessKey(), param.Bucket(), param.Object(), param.Action(), userPolicy.AuthorizedVols)
}
} else if (err == proto.ErrAccessKeyNotExists || err == proto.ErrUserNotExists) && volume != nil {
if ak, _ := volume.OSSSecure(); ak != param.AccessKey() {
@ -267,53 +241,142 @@ func (o *ObjectNode) policyCheck(f http.HandlerFunc) http.HandlerFunc {
}
isOwner = true
} else {
log.LogErrorf("policyCheck: load user policy from master fail: requestID(%v) accessKey(%v) err(%v)",
log.LogErrorf("user policy check: load user policy from master fail: requestID(%v) accessKey(%v) err(%v)",
GetRequestID(r), param.AccessKey(), err)
allowed = false
return
}
var vol *Volume
var acl *AccessControlPolicy
var policy *Policy
var loadBucketMeta = func(bucket string) (err error) {
if vol, err = o.getVol(bucket); err != nil {
// copy api should check srcBucket policy additionally
if param.apiName == COPY_OBJECT || param.apiName == UPLOAD_PART_COPY {
err := o.allowedBySrcBucketPolicy(param, userInfo.UserID)
if err != nil {
return
}
if acl, err = vol.metaLoader.loadACL(); err != nil {
return
}
if policy, err = vol.metaLoader.loadPolicy(); err != nil {
return
}
return
}
if err = loadBucketMeta(param.Bucket()); err != nil {
log.LogErrorf("policyCheck: load bucket metadata fail: requestID(%v) err(%v)", GetRequestID(r), err)
//step3. Check bucket policy
policycheck:
vol, acl, policy, vv, err := o.loadBucketMeta(param.Bucket())
if err != nil {
log.LogErrorf("bucket policy check: load bucket metadata fail: requestID(%v) err(%v)", GetRequestID(r), err)
allowed = false
ec = NoSuchBucket
return
}
if vol != nil && policy != nil && !policy.IsEmpty() {
allowed = policy.IsAllowed(param, isOwner)
if !allowed {
log.LogWarnf("policyCheck: bucket policy not allowed: requestID(%v) userID(%v) accessKey(%v) volume(%v) action(%v)",
GetRequestID(r), userInfo, param.AccessKey(), param.Bucket(), param.Action())
log.LogDebugf("bucket policy check: requestID(%v) policy(%v)", GetRequestID(r), policy)
conditionCheck := map[string]string{
SOURCEIP: param.sourceIP,
REFERER: param.r.Referer(),
HOST: param.r.Host,
}
if !IsBucketApi(param.apiName) {
conditionCheck[KEYNAME] = param.object
}
pcr := policy.IsAllowed(param, userInfo.UserID, vv.Owner, conditionCheck)
switch pcr {
case POLICY_ALLOW:
allowed = true
log.LogDebugf("bucket policy check: policy allowed: requestID(%v)", GetRequestID(r))
return
case POLICY_DENY:
allowed = false
log.LogWarnf("bucket policy check: policy not allowed: requestID(%v) ", GetRequestID(r))
return
case POLICY_UNKNOW:
// policy check result is unknown so that acl should be checked
log.LogWarnf("bucket policy check: policy unknown: requestID(%v) ", GetRequestID(r))
}
}
if vol != nil && acl != nil && !acl.IsAclEmpty() {
//step4. Check acl
//The default bucket acl should not be empty
if acl == nil && !isOwner {
allowed = false
log.LogWarnf("bucket acl check: not allowed because of empty bucket ACL : requestID(%v) ", GetRequestID(r))
return
}
if acl != nil && !acl.IsAclEmpty() {
allowed = acl.IsAllowed(param, isOwner)
if !allowed {
log.LogWarnf("policyCheck: bucket ACL not allowed: requestID(%v) userID(%v) accessKey(%v) volume(%v) action(%v)",
GetRequestID(r), userInfo, param.AccessKey(), param.Bucket(), param.Action())
log.LogWarnf("bucket acl check: bucket ACL not allowed: requestID(%v) acl(%v) accessKey(%v) volume(%v) action(%v)",
GetRequestID(r), userInfo, acl, param.Bucket(), param.Action())
return
}
}
allowed = true
log.LogDebugf("policyCheck: action allowed: requestID(%v) userID(%v) accessKey(%v) volume(%v) action(%v)",
log.LogDebugf("bucket acl check: action allowed: requestID(%v) userID(%v) accessKey(%v) volume(%v) action(%v)",
GetRequestID(r), userInfo, param.AccessKey(), param.Bucket(), param.Action())
}
}
func (o *ObjectNode) loadBucketMeta(bucket string) (vol *Volume, acl *AccessControlPolicy,
policy *Policy, vv *proto.VolView, err error) {
if vol, err = o.getVol(bucket); err != nil {
return
}
if acl, err = vol.metaLoader.loadACL(); err != nil {
return
}
if policy, err = vol.metaLoader.loadPolicy(); err != nil {
return
}
if vv, err = o.mc.ClientAPI().GetVolumeWithoutAuthKey(bucket); err != nil {
return
}
return
}
func (o *ObjectNode) allowedBySrcBucketPolicy(param *RequestParam, reqUid string) (err error) {
srcBucketId, srcKey, _, err := extractSrcBucketKey(param.r)
if err != nil {
log.LogDebugf("copySource(%v) argument invalid: requestID(%v)", param.r.Header.Get(HeaderNameXAmzCopySource), GetRequestID(param.r))
return
}
vol, acl, policy, vv, err := o.loadBucketMeta(srcBucketId)
if err != nil {
log.LogErrorf("srcBucket policy check: load bucket metadata fail: requestID(%v) err(%v)", GetRequestID(param.r), err)
return
}
if vol != nil && policy != nil && !policy.IsEmpty() {
conditionCheck := map[string]string{
SOURCEIP: param.sourceIP,
KEYNAME: srcKey,
REFERER: param.r.Referer(),
HOST: param.r.Host,
}
pcr := policy.IsAllowed(param, reqUid, vv.Owner, conditionCheck)
switch pcr {
case POLICY_ALLOW:
log.LogDebugf("srcBucket policy check: policy allowed: requestID(%v)", GetRequestID(param.r))
return
case POLICY_DENY:
log.LogWarnf("srcBucket policy check: policy not allowed: requestID(%v) ", GetRequestID(param.r))
return AccessDenied
case POLICY_UNKNOW:
// policy check result is unknown so that acl should be checked
log.LogWarnf("srcBucket policy check: policy unknown: requestID(%v) ", GetRequestID(param.r))
}
}
isOwner := reqUid == vv.Owner
if acl == nil && !isOwner {
log.LogWarnf("srcBucket acl check: not allowed because of empty bucket ACL : requestID(%v) ", GetRequestID(param.r))
return AccessDenied
}
if vol != nil && acl != nil && !acl.IsAclEmpty() {
allowed := acl.IsAllowed(param, isOwner)
if !allowed {
log.LogWarnf("srcBucket acl check: bucket ACL not allowed: requestID(%v) acl(%v) volume(%v) action(%v)",
GetRequestID(param.r), acl, param.Bucket(), param.Action())
return
}
}
log.LogDebugf("srcBucket acl check: action allowed: requestID(%v) accessKey(%v) volume(%v) action(%v)",
GetRequestID(param.r), param.AccessKey(), param.Bucket(), param.Action())
return
}

View File

@ -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

View File

@ -20,416 +20,266 @@ package objectnode
// https://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"sort"
"github.com/cubefs/cubefs/util/log"
)
//https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/example-bucket-policies.html
type ConditionValues map[string]StringSet
type Condition map[ConditionType]ConditionValues
type ConditionType string
type ConditionKey string
var (
invalidCIDR = "value %v must be CIDR string for %v condition"
invalidIPKey = "only %v key is allowed for %v condition"
invalidStringValue = "value must be a string for %v condition"
invalidConditionKey = "invalid condition key '%v'"
emptyCondition = "condition must not be empty"
cannotHandledCondition = "condition %v cannot be handled"
cannotTransformToString = "%v cannot transform to string"
cannotHandledConditionValue = "cannot handle condition values: %v"
invalidConditionOperator = "invalid condition operator '%v'"
invalidConditionValue = "invalid value"
duplicateConditionValue = "duplicate value found '%v'"
unknownJsonData = "unknown json data '%v'"
)
type operator string
// https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
const (
IpAddress ConditionType = "IpAddress"
NotIpAddress = "NotIpAddress"
StringLike = "StringLike"
StringNotLike = "StringNotLike"
StringEquals = "StringEquals"
StringNotEquals = "StringNotEquals"
Bool = "Bool"
DateEquals = "DateEquals"
DateNotEquals = "DateNotEquals"
DateLessThan = "DateLessThan"
DateLessThanEquals = "DateLessThanEquals"
DateGreaterThan = "DateGreaterThan"
DateGreaterThanEquals = "DateGreaterThanEquals"
NumericEquals = "NumericEquals"
NumericNotEquals = "NumericNotEquals"
NumericLessThan = "NumericLessThan"
NumericLessThanEquals = "NumericLessThanEquals"
NumericGreaterThan = "NumericGreaterThan"
NumericGreaterThanEquals = "NumericGreaterThanEquals"
ArnEquals = "ArnEquals"
ArnLike = "ArnLike" //
ArnNotEquals = "ArnNotEquals"
ArnNotLike = "ArnNotLike" //
stringLike = "StringLike"
stringNotLike = "StringNotLike"
ipAddress = "IpAddress"
notIPAddress = "NotIpAddress"
)
var (
StringFuncs = []ConditionFunc{StringEqualsFunc, StringNotEqualsFunc, StringLikeFunc, StringNotLikeFunc}
DateFuncs = []ConditionFunc{DateEqualsFunc, DateNotEqualsFunc, DateLessThanFunc, DateGreaterThanFunc, DateLessThanEqualsFunc, DateGreaterThanEqualsFunc}
IpAddressFuncs = []ConditionFunc{IpAddressFunc, NotIpAddressFunc}
BoolFuncs = []ConditionFunc{BoolFunc}
NumericFuncs = []ConditionFunc{NumericEqualsFunc, NumericNotEqualsFunc, NumericLessThanFunc, NumericLessThanEqualsFunc, NumericGreaterThanFunc, NumericGreaterThanEqualsFunc}
ArnFuncs = []ConditionFunc{ArnEqualsFunc, ArnNotEqualsFunc, ArnLikeFunc, ArnNotLikeFunc}
)
type ConditionTypeSet map[ConditionType]null
var (
StringType = ConditionTypeSet{StringEquals: void, StringNotEquals: void, StringLike: void, StringNotLike: void}
DateType = ConditionTypeSet{DateEquals: void, DateNotEquals: void, DateLessThan: void, DateGreaterThan: void, DateLessThanEquals: void, DateGreaterThanEquals: void}
IpAddressType = ConditionTypeSet{IpAddress: void, NotIpAddress: void}
BoolType = ConditionTypeSet{Bool: void}
NumericType = ConditionTypeSet{NumericEquals: void, NumericNotEquals: void, NumericLessThan: void, NumericLessThanEquals: void, NumericGreaterThan: void, NumericGreaterThanEquals: void}
ArnType = ConditionTypeSet{ArnEquals: void, ArnNotEquals: void, ArnLike: void, ArnNotLike: void}
)
// https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/reference_policies_condition-keys.html
const (
AwsCurrentTime ConditionKey = "aws:CurrentTime"
AwsEpochTime = "aws:EpochTime"
AwsMultiFactorAuthPresent = "aws:MultiFactorAuthPresent"
AwsPrincipalAccount = "aws:PrincipalAccount"
AwsPrincipalArn = "aws:PrincipalArn"
AwsPrincipalOrgID = "aws:PrincipalOrgID"
AwsPrincipalTag = "aws:PrincipalTag"
AwsPrincipalType = "aws:PrincipalType"
AwsReferer = "aws:Referer"
AwsRequestRegion = "aws:RequestRegion"
AwsRequestTagKey = "aws:RequestTag/tag-key"
AwsResourceTagKey = "aws:ResourceTag/tag-key"
AwsSecureTransport = "aws:SecureTransport"
AwsSourceAccout = "aws:AwsSourceAccout"
AwsSourceArn = "aws:SourceArn"
AwsSourceIp = "aws:SourceIp"
AwsSourceVpc = "aws:SourceVpc"
AwsSourceVpce = "aws:SourceVpce"
AwsTagKeys = "aws:TagKeys"
AwsTokenIssueTime = "aws:TokenIssueTime"
AwsUserAgent = "aws:UserAgent"
AwsUserId = "aws:userid"
AwsUserName = "aws:username"
AwsVpcSourceIp = "aws:VpcSourceIp"
)
var ConditionKeyType = map[ConditionKey]ConditionTypeSet{
AwsCurrentTime: DateType,
AwsEpochTime: DateType,
AwsMultiFactorAuthPresent: BoolType,
AwsPrincipalAccount: StringType,
AwsPrincipalArn: ArnType,
AwsPrincipalOrgID: StringType,
AwsPrincipalTag: StringType,
AwsPrincipalType: StringType,
AwsReferer: StringType,
AwsRequestRegion: StringType,
AwsRequestTagKey: StringType,
AwsResourceTagKey: StringType,
AwsSecureTransport: BoolType,
AwsSourceAccout: StringType,
AwsSourceArn: ArnType,
AwsSourceIp: IpAddressType,
AwsSourceVpc: StringType,
AwsSourceVpce: StringType,
AwsTagKeys: StringType,
AwsTokenIssueTime: DateType,
AwsUserAgent: StringType,
AwsUserId: StringType,
AwsUserName: StringType,
AwsVpcSourceIp: IpAddressType,
var supportedOperators = []operator{
stringLike,
stringNotLike,
ipAddress,
notIPAddress,
// Add new conditions here.
}
var ConditionFuncMap = map[ConditionType]ConditionFunc{
IpAddress: IpAddressFunc,
NotIpAddress: NotIpAddressFunc,
StringLike: StringLikeFunc,
StringNotLike: StringNotLikeFunc,
StringEquals: StringEqualsFunc,
StringNotEquals: StringNotEqualsFunc,
Bool: BoolFunc,
DateEquals: DateEqualsFunc,
DateNotEquals: DateNotEqualsFunc,
DateLessThan: DateLessThanFunc,
DateLessThanEquals: DateLessThanEqualsFunc,
DateGreaterThan: DateGreaterThanFunc,
DateGreaterThanEquals: DateGreaterThanEqualsFunc,
}
type ConditionFunc func(p *RequestParam, values ConditionValues) bool
var (
awsTrimedPrefix = []string{"aws:", "jwt:", "s3:"}
)
func TrimAwsPrefixKey(key string) string {
for _, prefix := range awsTrimedPrefix {
if strings.HasPrefix(key, prefix) {
return strings.TrimPrefix(key, prefix)
}
}
return key
}
func getCondtionValues(r *http.Request) map[string][]string {
currentTime := time.Now().UTC()
authInfo := parseRequestAuthInfo(r)
accessKey := authInfo.accessKey
principalType := "User"
if accessKey == "" {
principalType = "Anonymous"
}
values := map[string][]string{
"SourceIp": {getRequestIP(r)},
"UserAgent": {r.UserAgent()},
"Referer": {r.Referer()},
"CurrentTime": {currentTime.Format(AMZTimeFormat)},
"EpochTime": {fmt.Sprintf("%d", currentTime.Unix())},
"userid": {accessKey},
"username": {accessKey},
"PrincipalType": {principalType},
}
for k, v := range r.Header {
if existsV, ok := values[k]; ok {
values[k] = append(existsV, v...)
} else {
values[k] = v
}
}
for k, v := range r.URL.Query() {
if existsV, ok := values[k]; ok {
values[k] = append(existsV, v...)
} else {
values[k] = v
}
}
return values
}
func IpAddressFunc(p *RequestParam, value ConditionValues) bool {
key := TrimAwsPrefixKey(AwsSourceIp)
canonicalKey := http.CanonicalHeaderKey(key)
sourceIP, ok := value[canonicalKey]
if !ok {
return false
}
for ipnet := range sourceIP.values {
if ok, _ := isIPNetContainsIP(p.sourceIP, ipnet); ok {
func (op operator) IsValid() bool {
for _, supOp := range supportedOperators {
if op == supOp {
return true
}
}
return true
}
func NotIpAddressFunc(p *RequestParam, values ConditionValues) bool {
return !IpAddressFunc(p, values)
}
func StringLikeFunc(reqParam *RequestParam, storeCondVals ConditionValues) bool {
for k, storeVals := range storeCondVals {
key := TrimAwsPrefixKey(k)
canonicalKey := http.CanonicalHeaderKey(key)
if reqVals, ok := reqParam.conditionVars[canonicalKey]; ok {
for _, rv := range reqVals {
for sv := range storeVals.values {
if match := patternMatch(rv, sv); match {
return true
}
}
}
}
}
return false
}
func StringNotLikeFunc(p *RequestParam, values ConditionValues) bool {
return !StringLikeFunc(p, values)
}
func StringEqualsFunc(reqParam *RequestParam, storeCondVals ConditionValues) bool {
for k, storeVals := range storeCondVals {
key := TrimAwsPrefixKey(k)
canonicalKey := http.CanonicalHeaderKey(key)
if reqVals, ok := reqParam.conditionVars[canonicalKey]; ok {
for _, rv := range reqVals {
if storeVals.Contains(rv) {
return true
}
}
} else if reqVals, ok := reqParam.conditionVars[key]; ok {
for _, rv := range reqVals {
if storeVals.Contains(rv) {
return true
}
}
}
// encodes operator to JSON data.
func (op operator) MarshalJSON() ([]byte, error) {
if !op.IsValid() {
return nil, fmt.Errorf("invalid operator %v", op)
}
return false
return json.Marshal(string(op))
}
func StringNotEqualsFunc(p *RequestParam, values ConditionValues) bool {
return !StringNotEqualsFunc(p, values)
}
// check statement conditions
func (s Statement) checkConditions(param *RequestParam) bool {
if len(s.Condition) == 0 {
return true
// decodes JSON data to condition operator.
func (op *operator) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
for k, v := range s.Condition {
f, ok := ConditionFuncMap[k]
if !ok {
continue
}
if !f(param, v) {
parsedOperator, err := parseOperator(s)
if err != nil {
return err
}
*op = parsedOperator
return nil
}
func parseOperator(s string) (operator, error) {
n := operator(s)
if n.IsValid() {
return n, nil
}
return n, fmt.Errorf(invalidConditionOperator, s)
}
type Operation interface {
// evaluates this condition operation with given values.
evaluate(values map[string]string) bool
// returns all condition keys used in this operation.
keys() KeySet
// returns condition operator of this operation.
operator() operator
toMap() map[Key]ValueSet
}
type Condition []Operation
// evaluates all operation with given values map.
func (operations Condition) Evaluate(values map[string]string) bool {
for _, op := range operations {
if !op.evaluate(values) {
log.LogDebugf("cannot match a condition, operator:%v, conditionKV:%v, values: %v", op.operator(), op.toMap(), values)
return false
}
}
return true
}
func BoolFunc(p *RequestParam, policyCondtion ConditionValues) bool {
if len(policyCondtion) == 0 {
return true
// returns list of keys used in all operation.
func (operations Condition) Keys() KeySet {
keySet := NewKeySet()
for _, op := range operations {
keySet.AddAll(op.keys())
}
for condKey, condVal := range policyCondtion {
for vals := range condVal.values {
val1, _ := strconv.ParseBool(vals)
if cond, ok := p.conditionVars[condKey]; ok {
for _, c := range cond {
val2, _ := strconv.ParseBool(c)
return val1 == val2
}
return keySet
}
// encodes Condition to JSON data.
func (operations Condition) MarshalJSON() ([]byte, error) {
conditionKV := make(map[operator]map[Key]ValueSet)
for _, op := range operations {
if _, ok := conditionKV[op.operator()]; ok {
for k, v := range op.toMap() {
conditionKV[op.operator()][k] = v
}
} else {
conditionKV[op.operator()] = op.toMap()
}
}
return false
return json.Marshal(conditionKV)
}
func DateEqualsFunc(p *RequestParam, policyVals ConditionValues) bool {
for k, pVals := range policyVals {
for pVal := range pVals.values {
pDate, err := time.Parse(AMZTimeFormat, pVal)
func (operations Condition) String() string {
var opStrings []string
for _, op := range operations {
s := fmt.Sprintf("%v", op)
opStrings = append(opStrings, s)
}
sort.Strings(opStrings)
return fmt.Sprintf("%v", opStrings)
}
var conditionOpMap = map[operator]func(map[Key]ValueSet) (Operation, error){
stringLike: newStringLikeOp,
stringNotLike: newStringNotLikeOp,
ipAddress: newIPAddressOp,
notIPAddress: newNotIPAddressOp,
// Add new conditions here.
}
// decodes JSON data to Condition.
func (operations *Condition) UnmarshalJSON(data []byte) error {
operatorMap := make(map[string]map[string]ValueSet)
if err := json.Unmarshal(data, &operatorMap); err != nil {
return err
}
if len(operatorMap) == 0 {
return fmt.Errorf(emptyCondition)
}
var ops []Operation
for operatorString, args := range operatorMap {
o, err := parseOperator(operatorString)
if err != nil {
return err
}
vfn, ok := conditionOpMap[o]
if !ok {
return fmt.Errorf(cannotHandledCondition, o)
}
m := make(map[Key]ValueSet)
for keyString, values := range args {
key, err := parseKey(keyString)
if err != nil {
return false
return err
}
if reqVals, ok := p.conditionVars[k]; ok {
for _, reqVal := range reqVals {
reqDate, err := time.Parse(AMZTimeFormat, reqVal)
if err != nil {
return false
}
return pDate.Equal(reqDate)
}
m[key] = values
}
op, err := vfn(m)
if err != nil {
return err
}
ops = append(ops, op)
}
*operations = ops
return nil
}
func (operations Condition) CheckValid() error {
for _, op := range operations {
if !op.operator().IsValid() {
return NewError("InvalidConditionType", fmt.Sprintf("policy has invalid condition type: %s", op.operator()), 400)
}
for key := range op.keys() {
if !key.IsValid() {
return NewError("InvalidConditionKey", fmt.Sprintf("policy has invalid condition key: %s", key), 400)
}
}
}
return false
return nil
}
func DateNotEqualsFunc(p *RequestParam, value ConditionValues) bool {
return !DateEqualsFunc(p, value)
}
// parse a map into Condition
func (operations *Condition) parseOperations(operatorMap map[string]map[string]interface{}) error {
var ops []Operation
for operatorString, args := range operatorMap {
o, err := parseOperator(operatorString)
if err != nil {
return err
}
func DateLessThanFunc(p *RequestParam, value ConditionValues) bool {
for k, pVals := range value {
for pVal := range pVals.values {
pDate, err := time.Parse(AMZTimeFormat, pVal)
vfn, ok := conditionOpMap[o]
if !ok {
return fmt.Errorf(cannotHandledCondition, o)
}
m := make(map[Key]ValueSet)
for keyString, values := range args {
valueSet := NewValueSet()
key, err := parseKey(keyString)
if err != nil {
return false
return err
}
if reqVals, ok := p.conditionVars[k]; ok {
for _, reqVal := range reqVals {
reqDate, err := time.Parse(AMZTimeFormat, reqVal)
if err != nil {
return false
switch values.(type) {
case string:
valueSet.Add(NewStringValue(values.(string)))
case []interface{}:
for _, value := range values.([]interface{}) {
if valueString, ok := value.(string); ok {
valueSet.Add(NewStringValue(valueString))
} else {
return fmt.Errorf(cannotTransformToString, value)
}
return reqDate.Before(pDate)
}
default:
return fmt.Errorf(cannotHandledConditionValue, values)
}
m[key] = valueSet
}
}
return false
}
func DateLessThanEqualsFunc(p *RequestParam, value ConditionValues) bool {
for k, pVals := range value {
for pVal := range pVals.values {
pDate, err := time.Parse(AMZTimeFormat, pVal)
if err != nil {
return false
}
if reqVals, ok := p.conditionVars[k]; ok {
for _, reqVal := range reqVals {
reqDate, err := time.Parse(AMZTimeFormat, reqVal)
if err != nil {
return false
}
return reqDate.Before(pDate) || reqDate.Equal(pDate)
}
}
op, err := vfn(m)
if err != nil {
return err
}
ops = append(ops, op)
}
return true
}
func DateGreaterThanFunc(p *RequestParam, value ConditionValues) bool {
return !DateLessThanEqualsFunc(p, value)
}
func DateGreaterThanEqualsFunc(p *RequestParam, value ConditionValues) bool {
return !DateLessThanFunc(p, value)
}
func NumericEqualsFunc(p *RequestParam, value ConditionValues) bool {
//TODO: numeric equal implements
return true
}
func NumericNotEqualsFunc(p *RequestParam, value ConditionValues) bool {
return !NumericEqualsFunc(p, value)
}
func NumericLessThanFunc(p *RequestParam, value ConditionValues) bool {
//TODO: numeric less than implements
return true
}
func NumericLessThanEqualsFunc(p *RequestParam, value ConditionValues) bool {
return NumericLessThanFunc(p, value) || NumericEqualsFunc(p, value)
}
func NumericGreaterThanFunc(p *RequestParam, value ConditionValues) bool {
return !NumericLessThanEqualsFunc(p, value)
}
func NumericGreaterThanEqualsFunc(p *RequestParam, value ConditionValues) bool {
return !NumericLessThanFunc(p, value)
}
func ArnEqualsFunc(p *RequestParam, value ConditionValues) bool {
//TODO: numeric equal implements
return true
}
func ArnNotEqualsFunc(p *RequestParam, value ConditionValues) bool {
return !ArnEqualsFunc(p, value)
}
func ArnLikeFunc(p *RequestParam, value ConditionValues) bool {
//TODO: numeric equal implements
return true
}
func ArnNotLikeFunc(p *RequestParam, value ConditionValues) bool {
return !ArnLikeFunc(p, value)
*operations = ops
return nil
}

View 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
View 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
}

View 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 &notIPAddressOp{ipAddressOp{m: m}}, nil
}

View 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
View 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
}

View 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
View 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)
}

View 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)
}
}

View File

@ -17,29 +17,37 @@
package objectnode
import (
"strings"
"github.com/cubefs/cubefs/util/log"
)
// https://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
// https://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html
//https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/example-bucket-policies.html
type Effect string
type Principal map[string]StringSet
type Resource string
const (
Allow Effect = "Allow"
Deny = "Deny"
Allow = "Allow"
Deny = "Deny"
)
type Statement struct {
Sid string `json:"Sid,omitempty"`
Effect Effect `json:"Effect"`
Principal Principal `json:"Principal"`
Actions StringSet `json:"Action,omitempty"`
NotActions StringSet `json:"NotAction,omitempty"`
Resources StringSet `json:"Resource,omitempty"`
NotResources StringSet `json:"NotResource,omitempty"`
Condition Condition `json:"Condition,omitempty"`
Sid string `json:"Sid"`
Effect string `json:"Effect"`
Principal interface{} `json:"Principal"` // map or string
Action interface{} `json:"Action"` // []string or string
Resource interface{} `json:"Resource"` // []string or string
Condition Condition `json:"Condition,omitempty"`
}
func (s Statement) IsAllowed(p *RequestParam) bool {
return s.Effect == Allow
}
func (s *Statement) Validate(bucket string) (bool, error) {
@ -47,71 +55,260 @@ func (s *Statement) Validate(bucket string) (bool, error) {
}
func (s *Statement) isValid(bucket string) (bool, error) {
//TODO:
log.LogDebug("start to validate statement")
// step 1: check required field
if err := s.checkRequiredField(); err != nil {
return false, err
}
//step2: check each field
if !s.isEffectValid() {
return false, ErrInvalidEffectValue
}
if !s.isPrincipalValid() {
return false, ErrInvalidPricipalInPolicy
}
if !s.isActionValid() {
return false, ErrInvalidActionInPolicy
}
if !s.isResourceValid(bucket) {
return false, ErrInvalidResourceInPolicy
}
//step3: check action & resource valid combination
if !s.isValidCombination() {
return false, ErrInvalidActionResourceCombination
}
return true, nil
}
func (s Statement) IsAllowed(p *RequestParam) bool {
checked := s.check(p)
if s.Effect != Allow {
return !checked
func (s *Statement) checkRequiredField() error {
switch {
case s.Effect == "":
return ErrMissingEffectInPolicy
case s.Principal == nil:
return ErrMissingPrincipalInPolicy
case s.Action == nil:
return ErrMissingActionInPolicy
case s.Resource == nil:
return ErrMissingResourceInPolicy
default:
return nil
}
return checked
}
type CheckFuncs func(p *RequestParam) bool
func (s Statement) check(p *RequestParam) bool {
checkFuncs := []CheckFuncs{
s.checkPrincipal,
s.checkNotActions,
s.checkActions,
s.checkNotResources,
s.checkResources,
s.checkConditions,
func (s *Statement) isEffectValid() bool {
e := strings.ToLower(s.Effect)
if e == "allow" || e == "deny" {
return true
}
for _, f := range checkFuncs {
checked := f(p)
if !checked {
return false
}
// "Principal": "*" or "Principal" : {"AWS":"111122223333"} or "Principal" : {"AWS":["111122223333","444455556666"]}
func (s *Statement) isPrincipalValid() bool {
//principal: uid must be "*" or uint32, and can't be 0
switch s.Principal.(type) {
case string: // "*" or "123"
p := s.Principal.(string)
if !PrincipalElementType(p).valid() {
return false
}
}
return true
}
func (s Statement) checkPrincipal(p *RequestParam) bool {
if len(s.Principal) == 0 {
return true
}
for _, principal := range s.Principal {
if principal.ContainsWild(p.AccessKey()) {
return true
case map[string]interface{}:
p := s.Principal.(map[string]interface{})
if !PrincipalType(p).valid() {
return false
}
}
return false
}
func (s Statement) checkResources(p *RequestParam) bool {
if s.Resources.Empty() {
return true
}
if s.Resources.ContainsRegex(p.resource) {
return true
}
return false
}
func (s Statement) checkNotResources(p *RequestParam) bool {
if s.NotResources.Empty() {
return true
}
if s.NotResources.ContainsRegex(p.resource) {
default:
return false
}
return true
}
func (p PrincipalType) valid() bool {
p1, ok := p[S3_PRINCIPAL_PREFIX]
if !ok {
return false
}
switch p1.(type) {
case []interface{}:
p2 := p1.([]interface{})
if len(p2) == 0 {
return false
}
for _, p3 := range p2 {
p4, ok := p3.(string)
if !ok {
return false
}
if !PrincipalElementType(p4).valid() {
return false
}
}
return true
case string:
if !PrincipalElementType(p1.(string)).valid() {
return false
}
default:
return false
}
return true
}
func (p PrincipalElementType) valid() bool {
return true
}
func (s *Statement) isActionValid() bool {
switch s.Action.(type) {
case []interface{}: //["s3:PutObject", "s3:GetObject","s3:DeleteObject"]
actions := s.Action.([]interface{})
return ActionType(actions).valid()
case string: //"s3:ListBucket"
action := s.Action.(string)
return ActionElementType(action).valid()
default:
return false
}
}
func (a ActionElementType) valid() bool {
a1 := strings.TrimPrefix(strings.ToLower(string(a)), S3_ACTION_PREFIX)
if a1 == ACTION_ANY {
return true
}
_, ok := S3ActionToApis[a1]
return ok
}
func (actions ActionType) valid() bool {
if len(actions) == 0 {
return false
}
for _, a := range actions {
a1, ok := a.(string)
if !ok {
return false
}
if !ActionElementType(a1).valid() {
return false
}
}
return true
}
func (s *Statement) isResourceValid(bucketId string) bool {
switch s.Resource.(type) {
case string: // "Resource":"arn:aws:s3:::bucket/*"
r := s.Resource.(string)
return ResourceElement(r).valid(bucketId)
case []interface{}: // "Resource":["arn:aws:s3:::bucket/abc/*"]
r := s.Resource.([]interface{})
if len(r) == 0 {
return false
}
for _, r1 := range r {
r2, ok := r1.(string)
if !ok {
return false
}
if !ResourceElement(r2).valid(bucketId) {
return false
}
}
return true
default:
return false
}
}
func (r ResourceElementType) valid(bucketId string) bool {
bucket_key := strings.SplitN(string(r), "/", 2)
if len(bucket_key) < 2 {
return r == ResourceElementType(bucketId)
}
if bucket_key[0] != bucketId { //bucketId in resource list must be same with current bucketId
return false
}
if bucket_key[1] == "" { // key can't be empty when bucket is followed by a slash
return false
}
return true
}
func (s *Statement) isValidCombination() bool {
//check action & resource valid combination
hasBucketFormat, hasObjectFormat := s.getResourceFormat()
switch s.Action.(type) {
case string:
action := s.Action.(string)
return ActionElementType(action).matchResource(hasBucketFormat, hasObjectFormat)
case []interface{}:
actions := s.Action.([]interface{})
return ActionType(actions).matchResource(hasBucketFormat, hasObjectFormat)
default:
return false
}
}
func (a ActionElementType) matchResource(hasBucketFormat, hasObjectFormat bool) bool {
a1 := strings.TrimPrefix(strings.ToLower(string(a)), S3_ACTION_PREFIX)
if a1 == ACTION_ANY { // when action = "*", resource can contain "bucket" or "bucket/key"
return hasBucketFormat || hasObjectFormat
}
if validBucketActions.Contain(string(a1)) {
return hasBucketFormat
}
return hasObjectFormat
}
func (actions ActionType) matchResource(hasBucketFormat, hasObjectFormat bool) bool {
if len(actions) == 0 {
return false
}
for _, a := range actions { //every action should match
if a1, ok := a.(string); ok {
if !ActionElementType(a1).matchResource(hasBucketFormat, hasObjectFormat) {
return false
}
}
}
return true
}
func (s *Statement) getResourceFormat() (hasBucketFormat, hasObjectFormat bool) {
switch s.Resource.(type) {
case string:
r := s.Resource.(string)
return ResourceElement(r).format()
case []interface{}:
r := s.Resource.([]interface{})
if len(r) == 0 {
return
}
for _, r1 := range r {
if r2, ok := r1.(string); ok {
isBucketFormat, isKeyFormat := ResourceElement(r2).format()
if isBucketFormat {
hasBucketFormat = true
}
if isKeyFormat {
hasObjectFormat = true
}
}
}
return
default:
return false, false
}
}
func (r ResourceElementType) format() (isBucketFormat, isKeyFormat bool) {
isBucketFormat = r.isBucketFormat()
isKeyFormat = r.isKeyFormat()
return
}

View 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)
}
}

View 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
}

View 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())
}

View 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
}

View 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)
}
}
}

View 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
}

View 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))
}
}
}
}

View File

@ -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
View 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
)

View File

@ -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
}