Enhance policy handling to support SSE and WORM (#5790)

- remove old bucket policy handling
- add new policy handling
- add new policy handling unit tests

This patch brings support to bucket policy to have more control not
limiting to anonymous.  Bucket owner controls to allow/deny any rest
API.

For example server side encryption can be controlled by allowing
PUT/GET objects with encryptions including bucket owner.
This commit is contained in:
Bala FA 2018-04-25 04:23:30 +05:30 committed by kannappanr
parent 21a3c0f482
commit 0d52126023
77 changed files with 9811 additions and 2633 deletions

View File

@ -181,6 +181,9 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
return nil, err return nil, err
} }
// Create new policy system.
globalPolicySys = NewPolicySys()
// Setup admin mgmt REST API handlers. // Setup admin mgmt REST API handlers.
adminRouter := mux.NewRouter() adminRouter := mux.NewRouter()
registerAdminRouter(adminRouter) registerAdminRouter(adminRouter)

View File

@ -953,8 +953,6 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
apiErr = ErrEntityTooSmall apiErr = ErrEntityTooSmall
case NotImplemented: case NotImplemented:
apiErr = ErrNotImplemented apiErr = ErrNotImplemented
case PolicyNotFound:
apiErr = ErrNoSuchBucketPolicy
case PartTooBig: case PartTooBig:
apiErr = ErrEntityTooLarge apiErr = ErrEntityTooLarge
case UnsupportedMetadata: case UnsupportedMetadata:

View File

@ -1,5 +1,5 @@
/* /*
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc. * Minio Cloud Storage, (C) 2015-2018 Minio, Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -27,7 +27,7 @@ import (
"strings" "strings"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/handlers" "github.com/minio/minio/pkg/policy"
) )
// Verify if request has JWT. // Verify if request has JWT.
@ -123,28 +123,84 @@ func checkAdminRequestAuthType(r *http.Request, region string) APIErrorCode {
return s3Err return s3Err
} }
func checkRequestAuthType(ctx context.Context, r *http.Request, bucket, policyAction, region string) APIErrorCode { func checkRequestAuthType(ctx context.Context, r *http.Request, action policy.Action, bucketName, objectName string) APIErrorCode {
reqAuthType := getRequestAuthType(r) isOwner := true
accountName := globalServerConfig.GetCredential().AccessKey
switch reqAuthType { switch getRequestAuthType(r) {
case authTypeUnknown:
return ErrAccessDenied
case authTypePresignedV2, authTypeSignedV2: case authTypePresignedV2, authTypeSignedV2:
// Signature V2 validation. if errorCode := isReqAuthenticatedV2(r); errorCode != ErrNone {
return isReqAuthenticatedV2(r) return errorCode
case authTypeSigned, authTypePresigned:
return isReqAuthenticated(r, region)
}
if reqAuthType == authTypeAnonymous && policyAction != "" {
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
resource, err := getResource(r.URL.Path, r.Host, globalDomainName)
if err != nil {
return ErrInternalError
} }
return enforceBucketPolicy(ctx, bucket, policyAction, resource, case authTypeSigned, authTypePresigned:
r.Referer(), handlers.GetSourceIP(r), r.URL.Query()) region := globalServerConfig.GetRegion()
switch action {
case policy.GetBucketLocationAction, policy.ListAllMyBucketsAction:
region = ""
}
if errorCode := isReqAuthenticated(r, region); errorCode != ErrNone {
return errorCode
}
default:
isOwner = false
accountName = ""
}
// LocationConstraint is valid only for CreateBucketAction.
var locationConstraint string
if action == policy.CreateBucketAction {
// To extract region from XML in request body, get copy of request body.
payload, err := ioutil.ReadAll(r.Body)
if err != nil {
logger.LogIf(ctx, err)
return ErrAccessDenied
}
// Populate payload to extract location constraint.
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
var s3Error APIErrorCode
locationConstraint, s3Error = parseLocationConstraint(r)
if s3Error != ErrNone {
return ErrAccessDenied
}
// Populate payload again to handle it in HTTP handler.
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
}
if globalPolicySys.IsAllowed(policy.Args{
AccountName: accountName,
Action: action,
BucketName: bucketName,
ConditionValues: getConditionValues(r, locationConstraint),
IsOwner: isOwner,
ObjectName: objectName,
}) {
return ErrNone
}
// As policy.ListBucketAction and policy.ListObjectsAction are same but different names,
// policy.ListBucketAction is used across the code but user may used policy.ListObjectsAction
// in bucket policy to denote the same. In below try again with policy.ListObjectsAction.
if action != policy.ListBucketAction {
return ErrAccessDenied
}
if globalPolicySys.IsAllowed(policy.Args{
AccountName: accountName,
Action: policy.ListObjectsAction,
BucketName: bucketName,
ConditionValues: getConditionValues(r, locationConstraint),
IsOwner: isOwner,
ObjectName: objectName,
}) {
return ErrNone
} }
// By default return ErrAccessDenied
return ErrAccessDenied return ErrAccessDenied
} }

View File

@ -20,6 +20,7 @@ import (
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/minio/minio/pkg/policy"
) )
// Validate all the ListObjects query arguments, returns an APIErrorCode // Validate all the ListObjects query arguments, returns an APIErrorCode
@ -64,7 +65,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
return return
} }
if s3Error := checkRequestAuthType(ctx, r, bucket, "s3:ListBucket", globalServerConfig.GetRegion()); s3Error != ErrNone { if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
@ -134,7 +135,7 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
return return
} }
if s3Error := checkRequestAuthType(ctx, r, bucket, "s3:ListBucket", globalServerConfig.GetRegion()); s3Error != ErrNone { if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }

View File

@ -17,7 +17,6 @@
package cmd package cmd
import ( import (
"context"
"encoding/base64" "encoding/base64"
"encoding/xml" "encoding/xml"
"io" "io"
@ -26,86 +25,16 @@ import (
"net/url" "net/url"
"path" "path"
"path/filepath" "path/filepath"
"reflect"
"strings" "strings"
"sync" "sync"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio-go/pkg/set"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/policy"
) )
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
// Enforces bucket policies for a bucket for a given tatusaction.
func enforceBucketPolicy(ctx context.Context, bucket, action, resource, referer, sourceIP string, queryParams url.Values) (s3Error APIErrorCode) {
// Verify if bucket actually exists
objAPI := newObjectLayerFn()
if err := checkBucketExist(ctx, bucket, objAPI); err != nil {
switch err.(type) {
case BucketNameInvalid:
// Return error for invalid bucket name.
return ErrInvalidBucketName
case BucketNotFound:
// For no bucket found we return NoSuchBucket instead.
return ErrNoSuchBucket
}
// Return internal error for any other errors so that we can investigate.
return ErrInternalError
}
// Fetch bucket policy, if policy is not set return access denied.
p, err := objAPI.GetBucketPolicy(ctx, bucket)
if err != nil {
return ErrAccessDenied
}
if reflect.DeepEqual(p, emptyBucketPolicy) {
return ErrAccessDenied
}
// Construct resource in 'arn:aws:s3:::examplebucket/object' format.
arn := bucketARNPrefix + strings.TrimSuffix(strings.TrimPrefix(resource, "/"), "/")
// Get conditions for policy verification.
conditionKeyMap := make(policy.ConditionKeyMap)
for queryParam := range queryParams {
conditionKeyMap[queryParam] = set.CreateStringSet(queryParams.Get(queryParam))
}
// Add request referer to conditionKeyMap if present.
if referer != "" {
conditionKeyMap["referer"] = set.CreateStringSet(referer)
}
// Add request source Ip to conditionKeyMap.
conditionKeyMap["ip"] = set.CreateStringSet(sourceIP)
// Validate action, resource and conditions with current policy statements.
if !bucketPolicyEvalStatements(action, arn, conditionKeyMap, p.Statements) {
return ErrAccessDenied
}
return ErrNone
}
// Check if the action is allowed on the bucket/prefix.
func isBucketActionAllowed(action, bucket, prefix string, objectAPI ObjectLayer) bool {
reqInfo := &logger.ReqInfo{BucketName: bucket}
reqInfo.AppendTags("prefix", prefix)
ctx := logger.SetReqInfo(context.Background(), reqInfo)
bp, err := objectAPI.GetBucketPolicy(ctx, bucket)
if err != nil {
return false
}
if reflect.DeepEqual(bp, emptyBucketPolicy) {
return false
}
resource := bucketARNPrefix + path.Join(bucket, prefix)
var conditionKeyMap map[string]set.StringSet
// Validate action, resource and conditions with current policy statements.
return bucketPolicyEvalStatements(action, resource, conditionKeyMap, bp.Statements)
}
// GetBucketLocationHandler - GET Bucket location. // GetBucketLocationHandler - GET Bucket location.
// ------------------------- // -------------------------
// This operation returns bucket location. // This operation returns bucket location.
@ -121,12 +50,7 @@ func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *
return return
} }
s3Error := checkRequestAuthType(ctx, r, bucket, "s3:GetBucketLocation", globalMinioDefaultRegion) if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketLocationAction, bucket, ""); s3Error != ErrNone {
if s3Error == ErrInvalidRegion {
// Clients like boto3 send getBucketLocation() call signed with region that is configured.
s3Error = checkRequestAuthType(ctx, r, "", "s3:GetBucketLocation", globalServerConfig.GetRegion())
}
if s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
@ -180,7 +104,7 @@ func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter,
return return
} }
if s3Error := checkRequestAuthType(ctx, r, bucket, "s3:ListBucketMultipartUploads", globalServerConfig.GetRegion()); s3Error != ErrNone { if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketMultipartUploadsAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
@ -228,16 +152,12 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
if api.CacheAPI() != nil { if api.CacheAPI() != nil {
listBuckets = api.CacheAPI().ListBuckets listBuckets = api.CacheAPI().ListBuckets
} }
// ListBuckets does not have any bucket action.
s3Error := checkRequestAuthType(ctx, r, "", "", globalMinioDefaultRegion) if s3Error := checkRequestAuthType(ctx, r, policy.ListAllMyBucketsAction, "", ""); s3Error != ErrNone {
if s3Error == ErrInvalidRegion {
// Clients like boto3 send listBuckets() call signed with region that is configured.
s3Error = checkRequestAuthType(ctx, r, "", "", globalServerConfig.GetRegion())
}
if s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
// Invoke the list buckets. // Invoke the list buckets.
bucketsInfo, err := listBuckets(ctx) bucketsInfo, err := listBuckets(ctx)
if err != nil { if err != nil {
@ -266,12 +186,12 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
return return
} }
var authError APIErrorCode var s3Error APIErrorCode
if authError = checkRequestAuthType(ctx, r, bucket, "s3:DeleteObject", globalServerConfig.GetRegion()); authError != ErrNone { if s3Error = checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, ""); s3Error != ErrNone {
// In the event access is denied, a 200 response should still be returned // In the event access is denied, a 200 response should still be returned
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html // http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
if authError != ErrAccessDenied { if s3Error != ErrAccessDenied {
writeErrorResponse(w, authError, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
} }
@ -326,7 +246,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
defer wg.Done() defer wg.Done()
// If the request is denied access, each item // If the request is denied access, each item
// should be marked as 'AccessDenied' // should be marked as 'AccessDenied'
if authError == ErrAccessDenied { if s3Error == ErrAccessDenied {
dErrs[i] = PrefixAccessDenied{ dErrs[i] = PrefixAccessDenied{
Bucket: bucket, Bucket: bucket,
Object: obj.ObjectName, Object: obj.ObjectName,
@ -411,16 +331,14 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
return return
} }
// PutBucket does not have any bucket action. vars := mux.Vars(r)
s3Error := checkRequestAuthType(ctx, r, "", "", globalServerConfig.GetRegion()) bucket := vars["bucket"]
if s3Error != ErrNone {
if s3Error := checkRequestAuthType(ctx, r, policy.CreateBucketAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
vars := mux.Vars(r)
bucket := vars["bucket"]
// Parse incoming location constraint. // Parse incoming location constraint.
location, s3Error := parseLocationConstraint(r) location, s3Error := parseLocationConstraint(r)
if s3Error != ErrNone { if s3Error != ErrNone {
@ -690,10 +608,11 @@ func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Re
return return
} }
if s3Error := checkRequestAuthType(ctx, r, bucket, "s3:ListBucket", globalServerConfig.GetRegion()); s3Error != ErrNone { if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketAction, bucket, ""); s3Error != ErrNone {
writeErrorResponseHeadersOnly(w, s3Error) writeErrorResponseHeadersOnly(w, s3Error)
return return
} }
getBucketInfo := objectAPI.GetBucketInfo getBucketInfo := objectAPI.GetBucketInfo
if api.CacheAPI() != nil { if api.CacheAPI() != nil {
getBucketInfo = api.CacheAPI().GetBucketInfo getBucketInfo = api.CacheAPI().GetBucketInfo
@ -710,20 +629,20 @@ func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Re
func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "DeleteBucket") ctx := newContext(r, "DeleteBucket")
vars := mux.Vars(r)
bucket := vars["bucket"]
objectAPI := api.ObjectAPI() objectAPI := api.ObjectAPI()
if objectAPI == nil { if objectAPI == nil {
writeErrorResponse(w, ErrServerNotInitialized, r.URL) writeErrorResponse(w, ErrServerNotInitialized, r.URL)
return return
} }
// DeleteBucket does not have any bucket action. if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketAction, bucket, ""); s3Error != ErrNone {
if s3Error := checkRequestAuthType(ctx, r, "", "", globalServerConfig.GetRegion()); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
vars := mux.Vars(r)
bucket := vars["bucket"]
deleteBucket := objectAPI.DeleteBucket deleteBucket := objectAPI.DeleteBucket
if api.CacheAPI() != nil { if api.CacheAPI() != nil {
deleteBucket = api.CacheAPI().DeleteBucket deleteBucket = api.CacheAPI().DeleteBucket
@ -734,13 +653,8 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
return return
} }
// Notify all peers (including self) to update in-memory state
for addr, err := range globalNotificationSys.UpdateBucketPolicy(bucket) {
logger.GetReqInfo(ctx).AppendTags("remotePeer", addr.Name)
logger.LogIf(ctx, err)
}
globalNotificationSys.RemoveNotification(bucket) globalNotificationSys.RemoveNotification(bucket)
globalPolicySys.Remove(bucket)
for addr, err := range globalNotificationSys.DeleteBucket(bucket) { for addr, err := range globalNotificationSys.DeleteBucket(bucket) {
logger.GetReqInfo(ctx).AppendTags("remotePeer", addr.Name) logger.GetReqInfo(ctx).AppendTags("remotePeer", addr.Name)
logger.LogIf(ctx, err) logger.LogIf(ctx, err)

View File

@ -155,7 +155,7 @@ func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName stri
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse, // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getReadOnlyBucketStatement` so that the // sets the bucket policy using the policy statement generated from `getReadOnlyBucketStatement` so that the
// unsigned request goes through and its validated again. // unsigned request goes through and its validated again.
ExecObjectLayerAPIAnonTest(t, obj, "TestGetBucketLocationHandler", bucketName, "", instanceType, apiRouter, anonReq, getReadOnlyBucketStatement) ExecObjectLayerAPIAnonTest(t, obj, "TestGetBucketLocationHandler", bucketName, "", instanceType, apiRouter, anonReq, getAnonReadOnlyBucketPolicy(bucketName))
// HTTP request for testing when `objectLayer` is set to `nil`. // HTTP request for testing when `objectLayer` is set to `nil`.
// There is no need to use an existing bucket and valid input for creating the request // There is no need to use an existing bucket and valid input for creating the request
@ -260,7 +260,7 @@ func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, api
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse, // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getReadOnlyBucketStatement` so that the // sets the bucket policy using the policy statement generated from `getReadOnlyBucketStatement` so that the
// unsigned request goes through and its validated again. // unsigned request goes through and its validated again.
ExecObjectLayerAPIAnonTest(t, obj, "TestHeadBucketHandler", bucketName, "", instanceType, apiRouter, anonReq, getReadOnlyBucketStatement) ExecObjectLayerAPIAnonTest(t, obj, "TestHeadBucketHandler", bucketName, "", instanceType, apiRouter, anonReq, getAnonReadOnlyBucketPolicy(bucketName))
// HTTP request for testing when `objectLayer` is set to `nil`. // HTTP request for testing when `objectLayer` is set to `nil`.
// There is no need to use an existing bucket and valid input for creating the request // There is no need to use an existing bucket and valid input for creating the request
@ -494,7 +494,7 @@ func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName s
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse, // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyBucketStatement` so that the // sets the bucket policy using the policy statement generated from `getWriteOnlyBucketStatement` so that the
// unsigned request goes through and its validated again. // unsigned request goes through and its validated again.
ExecObjectLayerAPIAnonTest(t, obj, "TestListMultipartUploadsHandler", bucketName, "", instanceType, apiRouter, anonReq, getWriteOnlyBucketStatement) ExecObjectLayerAPIAnonTest(t, obj, "TestListMultipartUploadsHandler", bucketName, "", instanceType, apiRouter, anonReq, getAnonWriteOnlyBucketPolicy(bucketName))
// HTTP request for testing when `objectLayer` is set to `nil`. // HTTP request for testing when `objectLayer` is set to `nil`.
// There is no need to use an existing bucket and valid input for creating the request // There is no need to use an existing bucket and valid input for creating the request
@ -592,7 +592,7 @@ func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, ap
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse, // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again. // unsigned request goes through and its validated again.
ExecObjectLayerAPIAnonTest(t, obj, "ListBucketsHandler", "", "", instanceType, apiRouter, anonReq, getWriteOnlyObjectStatement) ExecObjectLayerAPIAnonTest(t, obj, "ListBucketsHandler", "", "", instanceType, apiRouter, anonReq, getAnonWriteOnlyBucketPolicy("*"))
// HTTP request for testing when `objectLayer` is set to `nil`. // HTTP request for testing when `objectLayer` is set to `nil`.
// There is no need to use an existing bucket and valid input for creating the request // There is no need to use an existing bucket and valid input for creating the request
@ -793,31 +793,3 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
// `ExecObjectLayerAPINilTest` manages the operation. // `ExecObjectLayerAPINilTest` manages the operation.
ExecObjectLayerAPINilTest(t, nilBucket, nilObject, instanceType, apiRouter, nilReq) ExecObjectLayerAPINilTest(t, nilBucket, nilObject, instanceType, apiRouter, nilReq)
} }
func TestIsBucketActionAllowed(t *testing.T) {
ExecObjectLayerAPITest(t, testIsBucketActionAllowedHandler, []string{"BucketLocation"})
}
func testIsBucketActionAllowedHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
credentials auth.Credentials, t *testing.T) {
testCases := []struct {
// input.
action string
bucket string
prefix string
isGlobalPoliciesNil bool
// flag indicating whether the test should pass.
shouldPass bool
}{
{"s3:GetBucketLocation", "mybucket", "abc", true, false},
{"s3:ListObject", "mybucket", "abc", false, false},
}
for i, testCase := range testCases {
isAllowed := isBucketActionAllowed(testCase.action, testCase.bucket, testCase.prefix, obj)
if isAllowed != testCase.shouldPass {
t.Errorf("Case %d: Expected the response status to be `%t`, but instead found `%t`", i+1, testCase.shouldPass, isAllowed)
}
}
}

View File

@ -27,6 +27,7 @@ import (
"github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/event/target" "github.com/minio/minio/pkg/event/target"
xnet "github.com/minio/minio/pkg/net" xnet "github.com/minio/minio/pkg/net"
"github.com/minio/minio/pkg/policy"
) )
const ( const (
@ -43,6 +44,9 @@ var errNoSuchNotifications = errors.New("The specified bucket does not have buck
func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "GetBucketNotification") ctx := newContext(r, "GetBucketNotification")
vars := mux.Vars(r)
bucketName := vars["bucket"]
objAPI := api.ObjectAPI() objAPI := api.ObjectAPI()
if objAPI == nil { if objAPI == nil {
writeErrorResponse(w, ErrServerNotInitialized, r.URL) writeErrorResponse(w, ErrServerNotInitialized, r.URL)
@ -53,14 +57,12 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
writeErrorResponse(w, ErrNotImplemented, r.URL) writeErrorResponse(w, ErrNotImplemented, r.URL)
return return
} }
if s3Error := checkRequestAuthType(ctx, r, "", "", globalServerConfig.GetRegion()); s3Error != ErrNone {
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketNotificationAction, bucketName, ""); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
vars := mux.Vars(r)
bucketName := vars["bucket"]
_, err := objAPI.GetBucketInfo(ctx, bucketName) _, err := objAPI.GetBucketInfo(ctx, bucketName)
if err != nil { if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL) writeErrorResponse(w, toAPIErrorCode(err), r.URL)
@ -104,14 +106,15 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
writeErrorResponse(w, ErrNotImplemented, r.URL) writeErrorResponse(w, ErrNotImplemented, r.URL)
return return
} }
if s3Error := checkRequestAuthType(ctx, r, "", "", globalServerConfig.GetRegion()); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL)
return
}
vars := mux.Vars(r) vars := mux.Vars(r)
bucketName := vars["bucket"] bucketName := vars["bucket"]
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketNotificationAction, bucketName, ""); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL)
return
}
_, err := objectAPI.GetBucketInfo(ctx, bucketName) _, err := objectAPI.GetBucketInfo(ctx, bucketName)
if err != nil { if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL) writeErrorResponse(w, toAPIErrorCode(err), r.URL)
@ -166,14 +169,15 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
writeErrorResponse(w, ErrNotImplemented, r.URL) writeErrorResponse(w, ErrNotImplemented, r.URL)
return return
} }
if s3Error := checkRequestAuthType(ctx, r, "", "", globalServerConfig.GetRegion()); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL)
return
}
vars := mux.Vars(r) vars := mux.Vars(r)
bucketName := vars["bucket"] bucketName := vars["bucket"]
if s3Error := checkRequestAuthType(ctx, r, policy.ListenBucketNotificationAction, bucketName, ""); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL)
return
}
values := r.URL.Query() values := r.URL.Query()
var prefix string var prefix string

View File

@ -19,206 +19,24 @@ package cmd
import ( import (
"encoding/json" "encoding/json"
"io" "io"
"io/ioutil"
"net"
"net/http" "net/http"
"runtime"
"strings"
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/wildcard" "github.com/minio/minio/pkg/policy"
) )
// maximum supported access policy size. const (
const maxAccessPolicySize = 20 * humanize.KiByte // As per AWS S3 specification, 20KiB policy JSON data is allowed.
maxBucketPolicySize = 20 * humanize.KiByte
// Verify if a given action is valid for the url path based on the // Policy configuration file.
// existing bucket access policy. bucketPolicyConfig = "policy.json"
func bucketPolicyEvalStatements(action string, resource string, conditions policy.ConditionKeyMap, )
statements []policy.Statement) bool {
for _, statement := range statements {
if bucketPolicyMatchStatement(action, resource, conditions, statement) {
if statement.Effect == "Allow" {
return true
}
// Do not uncomment kept here for readability.
// else statement.Effect == "Deny"
return false
}
}
// None match so deny.
return false
}
// Verify if action, resource and conditions match input policy statement. // PutBucketPolicyHandler - This HTTP handler stores given bucket policy configuration as per
func bucketPolicyMatchStatement(action string, resource string, conditions policy.ConditionKeyMap, // https://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
statement policy.Statement) bool {
// Verify if action, resource and condition match in given statement.
return (bucketPolicyActionMatch(action, statement) &&
bucketPolicyResourceMatch(resource, statement) &&
bucketPolicyConditionMatch(conditions, statement))
}
// Verify if given action matches with policy statement.
func bucketPolicyActionMatch(action string, statement policy.Statement) bool {
return !statement.Actions.FuncMatch(actionMatch, action).IsEmpty()
}
// Match function matches wild cards in 'pattern' for resource.
func resourceMatch(pattern, resource string) bool {
if runtime.GOOS == "windows" {
// For windows specifically make sure we are case insensitive.
return wildcard.Match(strings.ToLower(pattern), strings.ToLower(resource))
}
return wildcard.Match(pattern, resource)
}
// Match function matches wild cards in 'pattern' for action.
func actionMatch(pattern, action string) bool {
return wildcard.MatchSimple(pattern, action)
}
func refererMatch(pattern, referer string) bool {
return wildcard.MatchSimple(pattern, referer)
}
// isIPInCIDR - checks if a given a IP address is a member of the given subnet.
func isIPInCIDR(cidr, ip string) bool {
// AWS S3 spec says IPs must use standard CIDR notation.
// http://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html#example-bucket-policies-use-case-3.
_, cidrNet, err := net.ParseCIDR(cidr)
if err != nil {
return false // If provided CIDR can't be parsed no IP will be in the subnet.
}
addr := net.ParseIP(ip)
return cidrNet.Contains(addr)
}
// Verify if given resource matches with policy statement.
func bucketPolicyResourceMatch(resource string, statement policy.Statement) bool {
// the resource rule for object could contain "*" wild card.
// the requested object can be given access based on the already set bucket policy if
// the match is successful.
// More info: http://docs.aws.amazon.com/AmazonS3/latest/dev/s3-arn-format.html.
return !statement.Resources.FuncMatch(resourceMatch, resource).IsEmpty()
}
// Verify if given condition matches with policy statement.
func bucketPolicyConditionMatch(conditions policy.ConditionKeyMap, statement policy.Statement) bool {
// Supports following conditions.
// - StringEquals
// - StringNotEquals
// - StringLike
// - StringNotLike
// - IpAddress
// - NotIpAddress
//
// Supported applicable condition keys for each conditions.
// - s3:prefix
// - s3:max-keys
// - s3:aws-Referer
// - s3:aws-SourceIp
// The following loop evaluates the logical AND of all the
// conditions in the statement. Note: we can break out of the
// loop if and only if a condition evaluates to false.
for condition, conditionKeyVal := range statement.Conditions {
prefixConditon := conditionKeyVal["s3:prefix"]
maxKeyCondition := conditionKeyVal["s3:max-keys"]
if condition == "StringEquals" {
// If there is no condition with "s3:prefix" or "s3:max-keys" condition key
// then there is nothing to check condition against.
if !prefixConditon.IsEmpty() && !prefixConditon.Equals(conditions["prefix"]) {
return false
}
if !maxKeyCondition.IsEmpty() && !maxKeyCondition.Equals(conditions["max-keys"]) {
return false
}
} else if condition == "StringNotEquals" {
// If there is no condition with "s3:prefix" or "s3:max-keys" condition key
// then there is nothing to check condition against.
if !prefixConditon.IsEmpty() && prefixConditon.Equals(conditions["prefix"]) {
return false
}
if !maxKeyCondition.IsEmpty() && maxKeyCondition.Equals(conditions["max-keys"]) {
return false
}
} else if condition == "StringLike" {
awsReferers := conditionKeyVal["aws:Referer"]
// Skip empty condition, it is trivially satisfied.
if awsReferers.IsEmpty() {
continue
}
// wildcard match of referer in statement was not empty.
// StringLike has a match, i.e, condition evaluates to true.
refererFound := false
for referer := range conditions["referer"] {
if !awsReferers.FuncMatch(refererMatch, referer).IsEmpty() {
refererFound = true
break
}
}
// No matching referer found, so the condition is false.
if !refererFound {
return false
}
} else if condition == "StringNotLike" {
awsReferers := conditionKeyVal["aws:Referer"]
// Skip empty condition, it is trivially satisfied.
if awsReferers.IsEmpty() {
continue
}
// wildcard match of referer in statement was not empty.
// StringNotLike has a match, i.e, condition evaluates to false.
for referer := range conditions["referer"] {
if !awsReferers.FuncMatch(refererMatch, referer).IsEmpty() {
return false
}
}
} else if condition == "IpAddress" {
awsIps := conditionKeyVal["aws:SourceIp"]
// Skip empty condition, it is trivially satisfied.
if awsIps.IsEmpty() {
continue
}
// wildcard match of ip if statement was not empty.
// Find a valid ip.
ipFound := false
for ip := range conditions["ip"] {
if !awsIps.FuncMatch(isIPInCIDR, ip).IsEmpty() {
ipFound = true
break
}
}
if !ipFound {
return false
}
} else if condition == "NotIpAddress" {
awsIps := conditionKeyVal["aws:SourceIp"]
// Skip empty condition, it is trivially satisfied.
if awsIps.IsEmpty() {
continue
}
// wildcard match of ip if statement was not empty.
// Find if nothing matches.
for ip := range conditions["ip"] {
if !awsIps.FuncMatch(isIPInCIDR, ip).IsEmpty() {
return false
}
}
}
}
return true
}
// PutBucketPolicyHandler - PUT Bucket policy
// -----------------
// This implementation of the PUT operation uses the policy
// subresource to add to or replace a policy on a bucket
func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "PutBucketPolicy") ctx := newContext(r, "PutBucketPolicy")
@ -228,65 +46,46 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
return return
} }
if s3Error := checkRequestAuthType(ctx, r, "", "", globalServerConfig.GetRegion()); s3Error != ErrNone { vars := mux.Vars(r)
bucket := vars["bucket"]
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketPolicyAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
vars := mux.Vars(r) // Check if bucket exists.
bucket := vars["bucket"] if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
// Before proceeding validate if bucket exists.
_, err := objAPI.GetBucketInfo(ctx, bucket)
if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL) writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return return
} }
// If Content-Length is unknown or zero, deny the // Error out if Content-Length is missing.
// request. PutBucketPolicy always needs a Content-Length. // PutBucketPolicy always needs Content-Length.
if r.ContentLength == -1 || r.ContentLength == 0 { if r.ContentLength <= 0 {
writeErrorResponse(w, ErrMissingContentLength, r.URL) writeErrorResponse(w, ErrMissingContentLength, r.URL)
return return
} }
// If Content-Length is greater than maximum allowed policy size.
if r.ContentLength > maxAccessPolicySize { // Error out if Content-Length is beyond allowed size.
if r.ContentLength > maxBucketPolicySize {
writeErrorResponse(w, ErrEntityTooLarge, r.URL) writeErrorResponse(w, ErrEntityTooLarge, r.URL)
return return
} }
// Read access policy up to maxAccessPolicySize. bucketPolicy, err := policy.ParseConfig(io.LimitReader(r.Body, r.ContentLength), bucket)
// http://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
// bucket policies are limited to 20KB in size, using a limit reader.
policyBytes, err := ioutil.ReadAll(io.LimitReader(r.Body, maxAccessPolicySize))
if err != nil { if err != nil {
logger.LogIf(ctx, err) writeErrorResponse(w, ErrMalformedPolicy, r.URL)
return
}
if err = objAPI.SetBucketPolicy(ctx, bucket, bucketPolicy); err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL) writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return return
} }
policyInfo := policy.BucketAccessPolicy{}
if err = json.Unmarshal(policyBytes, &policyInfo); err != nil {
writeErrorResponse(w, ErrInvalidPolicyDocument, r.URL)
return
}
// Parse check bucket policy.
if s3Error := checkBucketPolicyResources(bucket, policyInfo); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL)
return
}
if err = objAPI.SetBucketPolicy(ctx, bucket, policyInfo); err != nil { globalPolicySys.Set(bucket, *bucketPolicy)
switch err.(type) { for addr, err := range globalNotificationSys.SetBucketPolicy(bucket, bucketPolicy) {
case NotImplemented:
// Return error for invalid bucket name.
writeErrorResponse(w, ErrNotImplemented, r.URL)
default:
writeErrorResponse(w, ErrMalformedPolicy, r.URL)
}
return
}
for addr, err := range globalNotificationSys.UpdateBucketPolicy(bucket) {
logger.GetReqInfo(ctx).AppendTags("remotePeer", addr.Name) logger.GetReqInfo(ctx).AppendTags("remotePeer", addr.Name)
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
} }
@ -295,10 +94,7 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
writeSuccessNoContent(w) writeSuccessNoContent(w)
} }
// DeleteBucketPolicyHandler - DELETE Bucket policy // DeleteBucketPolicyHandler - This HTTP handler removes bucket policy configuration.
// -----------------
// This implementation of the DELETE operation uses the policy
// subresource to add to remove a policy on a bucket.
func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "DeleteBucketPolicy") ctx := newContext(r, "DeleteBucketPolicy")
@ -308,29 +104,27 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
return return
} }
if s3Error := checkRequestAuthType(ctx, r, "", "", globalServerConfig.GetRegion()); s3Error != ErrNone { vars := mux.Vars(r)
bucket := vars["bucket"]
if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketPolicyAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
vars := mux.Vars(r) // Check if bucket exists.
bucket := vars["bucket"] if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
// Before proceeding validate if bucket exists.
_, err := objAPI.GetBucketInfo(ctx, bucket)
if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL) writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return return
} }
// Delete bucket access policy, by passing an empty policy
// struct.
if err := objAPI.DeleteBucketPolicy(ctx, bucket); err != nil { if err := objAPI.DeleteBucketPolicy(ctx, bucket); err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL) writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return return
} }
for addr, err := range globalNotificationSys.UpdateBucketPolicy(bucket) { globalPolicySys.Remove(bucket)
for addr, err := range globalNotificationSys.RemoveBucketPolicy(bucket) {
logger.GetReqInfo(ctx).AppendTags("remotePeer", addr.Name) logger.GetReqInfo(ctx).AppendTags("remotePeer", addr.Name)
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
} }
@ -339,10 +133,7 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
writeSuccessNoContent(w) writeSuccessNoContent(w)
} }
// GetBucketPolicyHandler - GET Bucket policy // GetBucketPolicyHandler - This HTTP handler returns bucket policy configuration.
// -----------------
// This operation uses the policy
// subresource to return the policy of a specified bucket.
func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, "GetBucketPolicy") ctx := newContext(r, "GetBucketPolicy")
@ -352,29 +143,28 @@ func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *ht
return return
} }
if s3Error := checkRequestAuthType(ctx, r, "", "", globalServerConfig.GetRegion()); s3Error != ErrNone { vars := mux.Vars(r)
bucket := vars["bucket"]
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
vars := mux.Vars(r) // Check if bucket exists.
bucket := vars["bucket"] if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
// Before proceeding validate if bucket exists.
_, err := objAPI.GetBucketInfo(ctx, bucket)
if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL) writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return return
} }
// Read bucket access policy. // Read bucket access policy.
policy, err := objAPI.GetBucketPolicy(ctx, bucket) bucketPolicy, err := objAPI.GetBucketPolicy(ctx, bucket)
if err != nil { if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL) writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return return
} }
policyBytes, err := json.Marshal(&policy) policyData, err := json.Marshal(bucketPolicy)
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
writeErrorResponse(w, toAPIErrorCode(err), r.URL) writeErrorResponse(w, toAPIErrorCode(err), r.URL)
@ -382,5 +172,5 @@ func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *ht
} }
// Write to client. // Write to client.
w.Write(policyBytes) w.Write(policyData)
} }

View File

@ -24,222 +24,72 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect"
"strings"
"testing" "testing"
"github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio-go/pkg/set"
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/policy/condition"
) )
// Tests validate Bucket policy resource matcher. func getAnonReadOnlyBucketPolicy(bucketName string) *policy.Policy {
func TestBucketPolicyResourceMatch(t *testing.T) { return &policy.Policy{
Version: policy.DefaultVersion,
// generates statement with given resource.. Statements: []policy.Statement{policy.NewStatement(
generateStatement := func(resource string) policy.Statement { policy.Allow,
statement := policy.Statement{} policy.NewPrincipal("*"),
statement.Resources = set.CreateStringSet([]string{resource}...) policy.NewActionSet(policy.GetBucketLocationAction, policy.ListBucketAction),
return statement policy.NewResourceSet(policy.NewResource(bucketName, "")),
} condition.NewFunctions(),
)},
// generates resource prefix.
generateResource := func(bucketName, objectName string) string {
return bucketARNPrefix + bucketName + "/" + objectName
}
testCases := []struct {
resourceToMatch string
statement policy.Statement
expectedResourceMatch bool
}{
// Test case 1-4.
// Policy with resource ending with bucket/* allows access to all objects inside the given bucket.
{generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", bucketARNPrefix, "minio-bucket"+"/*")), true},
{generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", bucketARNPrefix, "minio-bucket"+"/*")), true},
{generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", bucketARNPrefix, "minio-bucket"+"/*")), true},
{generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", bucketARNPrefix, "minio-bucket"+"/*")), true},
// Test case - 5.
// Policy with resource ending with bucket/oo* should not allow access to bucket/output.txt.
{generateResource("minio-bucket", "output.txt"), generateStatement(fmt.Sprintf("%s%s", bucketARNPrefix, "minio-bucket"+"/oo*")), false},
// Test case - 6.
// Policy with resource ending with bucket/oo* should allow access to bucket/ootput.txt.
{generateResource("minio-bucket", "ootput.txt"), generateStatement(fmt.Sprintf("%s%s", bucketARNPrefix, "minio-bucket"+"/oo*")), true},
// Test case - 7.
// Policy with resource ending with bucket/oo* allows access to all sub-dirs starting with "oo" inside given bucket.
{generateResource("minio-bucket", "oop-bucket/my-file"), generateStatement(fmt.Sprintf("%s%s", bucketARNPrefix, "minio-bucket"+"/oo*")), true},
// Test case - 8.
{generateResource("minio-bucket", "Asia/India/1.pjg"), generateStatement(fmt.Sprintf("%s%s", bucketARNPrefix, "minio-bucket"+"/Asia/Japan/*")), false},
// Test case - 9.
{generateResource("minio-bucket", "Asia/India/1.pjg"), generateStatement(fmt.Sprintf("%s%s", bucketARNPrefix, "minio-bucket"+"/Asia/Japan/*")), false},
// Test case - 10.
// Proves that the name space is flat.
{generateResource("minio-bucket", "Africa/Bihar/India/design_info.doc/Bihar"), generateStatement(fmt.Sprintf("%s%s", bucketARNPrefix,
"minio-bucket"+"/*/India/*/Bihar")), true},
// Test case - 11.
// Proves that the name space is flat.
{generateResource("minio-bucket", "Asia/China/India/States/Bihar/output.txt"), generateStatement(fmt.Sprintf("%s%s", bucketARNPrefix,
"minio-bucket"+"/*/India/*/Bihar/*")), true},
}
for i, testCase := range testCases {
actualResourceMatch := bucketPolicyResourceMatch(testCase.resourceToMatch, testCase.statement)
if testCase.expectedResourceMatch != actualResourceMatch {
t.Errorf("Test %d: Expected Resource match to be `%v`, but instead found it to be `%v`", i+1, testCase.expectedResourceMatch, actualResourceMatch)
}
} }
} }
// TestBucketPolicyActionMatch - Test validates whether given action on the func getAnonWriteOnlyBucketPolicy(bucketName string) *policy.Policy {
// bucket/object matches the allowed actions in policy.Statement. return &policy.Policy{
// This test preserves the allowed actions for all 3 sets of policies, that is read-write,read-only, write-only. Version: policy.DefaultVersion,
// The intention of the test is to catch any changes made to allowed action for on eof the above 3 major policy groups mentioned. Statements: []policy.Statement{policy.NewStatement(
func TestBucketPolicyActionMatch(t *testing.T) { policy.Allow,
bucketName := getRandomBucketName() policy.NewPrincipal("*"),
objectPrefix := "test-object" policy.NewActionSet(
policy.GetBucketLocationAction,
testCases := []struct { policy.ListBucketMultipartUploadsAction,
action string ),
statement policy.Statement policy.NewResourceSet(policy.NewResource(bucketName, "")),
expectedResult bool condition.NewFunctions(),
}{ )},
// s3:GetBucketLocation is the action necessary to be present in the bucket policy to allow
// fetching of bucket location on an Anonymous/unsigned request.
//r ead-write bucket policy is expected to allow GetBucketLocation operation on an anonymous request (Test case - 1).
{"s3:GetBucketLocation", getReadWriteBucketStatement(bucketName, objectPrefix), true},
// write-only bucket policy is expected to allow GetBucketLocation operation on an anonymous request (Test case - 2).
{"s3:GetBucketLocation", getWriteOnlyBucketStatement(bucketName, objectPrefix), true},
// read-only bucket policy is expected to allow GetBucketLocation operation on an anonymous request (Test case - 3).
{"s3:GetBucketLocation", getReadOnlyBucketStatement(bucketName, objectPrefix), true},
// Any of the Object level access permissions shouldn't allow for GetBucketLocation operation on an Anonymous/unsigned request (Test cases 4-6).
{"s3:GetBucketLocation", getReadWriteObjectStatement(bucketName, objectPrefix), false},
{"s3:GetBucketLocation", getWriteOnlyObjectStatement(bucketName, objectPrefix), false},
{"s3:GetBucketLocation", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
// s3:ListBucketMultipartUploads is the action necessary to be present in the bucket policy to allow
// Listing of multipart uploads in a given bucket for an Anonymous/unsigned request.
//read-write bucket policy is expected to allow ListBucketMultipartUploads operation on an anonymous request (Test case 7).
{"s3:ListBucketMultipartUploads", getReadWriteBucketStatement(bucketName, objectPrefix), true},
// write-only bucket policy is expected to allow ListBucketMultipartUploads operation on an anonymous request (Test case 8).
{"s3:ListBucketMultipartUploads", getWriteOnlyBucketStatement(bucketName, objectPrefix), true},
// read-only bucket policy is expected to not allow ListBucketMultipartUploads operation on an anonymous request (Test case 9).
// the allowed actions in read-only bucket statement are "s3:GetBucketLocation","s3:ListBucket",
// this should not allow for ListBucketMultipartUploads operations.
{"s3:ListBucketMultipartUploads", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
// Any of the object level policy will not allow for s3:ListBucketMultipartUploads (Test cases 10-12).
{"s3:ListBucketMultipartUploads", getReadWriteObjectStatement(bucketName, objectPrefix), false},
{"s3:ListBucketMultipartUploads", getWriteOnlyObjectStatement(bucketName, objectPrefix), false},
{"s3:ListBucketMultipartUploads", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
// s3:ListBucket is the action necessary to be present in the bucket policy to allow
// listing of all objects inside a given bucket on an Anonymous/unsigned request.
// Cases for testing ListBucket access for different Bucket level access permissions.
// read-only bucket policy is expected to allow ListBucket operation on an anonymous request (Test case 13).
{"s3:ListBucket", getReadOnlyBucketStatement(bucketName, objectPrefix), true},
// read-write bucket policy is expected to allow ListBucket operation on an anonymous request (Test case 14).
{"s3:ListBucket", getReadWriteBucketStatement(bucketName, objectPrefix), true},
// write-only bucket policy is expected to not allow ListBucket operation on an anonymous request (Test case 15).
// the allowed actions in write-only bucket statement are "s3:GetBucketLocation", "s3:ListBucketMultipartUploads",
// this should not allow for ListBucket operations.
{"s3:ListBucket", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
// Cases for testing ListBucket access for different Object level access permissions (Test cases 16-18).
// Any of the Object level access permissions shouldn't allow for ListBucket operation on an Anonymous/unsigned request.
{"s3:ListBucket", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
{"s3:ListBucket", getReadWriteObjectStatement(bucketName, objectPrefix), false},
{"s3:ListBucket", getWriteOnlyObjectStatement(bucketName, objectPrefix), false},
// s3:DeleteObject is the action necessary to be present in the bucket policy to allow
// deleting/removal of objects inside a given bucket for an Anonymous/unsigned request.
// Cases for testing DeleteObject access for different Bucket level access permissions (Test cases 19-21).
// Any of the Bucket level access permissions shouldn't allow for DeleteObject operation on an Anonymous/unsigned request.
{"s3:DeleteObject", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
{"s3:DeleteObject", getReadWriteBucketStatement(bucketName, objectPrefix), false},
{"s3:DeleteObject", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
// Cases for testing DeleteObject access for different Object level access permissions (Test cases 22).
// read-only bucket policy is expected to not allow Delete Object operation on an anonymous request.
{"s3:DeleteObject", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
// read-write bucket policy is expected to allow Delete Bucket operation on an anonymous request (Test cases 23).
{"s3:DeleteObject", getReadWriteObjectStatement(bucketName, objectPrefix), true},
// write-only bucket policy is expected to allow Delete Object operation on an anonymous request (Test cases 24).
{"s3:DeleteObject", getWriteOnlyObjectStatement(bucketName, objectPrefix), true},
// s3:AbortMultipartUpload is the action necessary to be present in the bucket policy to allow
// cancelling or abortion of an already initiated multipart upload operation for an Anonymous/unsigned request.
// Cases for testing AbortMultipartUpload access for different Bucket level access permissions (Test cases 25-27).
// Any of the Bucket level access permissions shouldn't allow for AbortMultipartUpload operation on an Anonymous/unsigned request.
{"s3:AbortMultipartUpload", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
{"s3:AbortMultipartUpload", getReadWriteBucketStatement(bucketName, objectPrefix), false},
{"s3:AbortMultipartUpload", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
// Cases for testing AbortMultipartUpload access for different Object level access permissions.
// read-only object policy is expected to not allow AbortMultipartUpload operation on an anonymous request (Test case 28).
{"s3:AbortMultipartUpload", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
// read-write object policy is expected to allow AbortMultipartUpload operation on an anonymous request (Test case 29).
{"s3:AbortMultipartUpload", getReadWriteObjectStatement(bucketName, objectPrefix), true},
// write-only object policy is expected to allow AbortMultipartUpload operation on an anonymous request (Test case 30).
{"s3:AbortMultipartUpload", getWriteOnlyObjectStatement(bucketName, objectPrefix), true},
// s3:PutObject is the action necessary to be present in the bucket policy to allow
// uploading of an object for an Anonymous/unsigned request.
// Cases for testing PutObject access for different Bucket level access permissions (Test cases 31-33).
// Any of the Bucket level access permissions shouldn't allow for PutObject operation on an Anonymous/unsigned request.
{"s3:PutObject", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
{"s3:PutObject", getReadWriteBucketStatement(bucketName, objectPrefix), false},
{"s3:PutObject", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
// Cases for testing PutObject access for different Object level access permissions.
// read-only object policy is expected to not allow PutObject operation on an anonymous request (Test case 34).
{"s3:PutObject", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
// read-write object policy is expected to allow PutObject operation on an anonymous request (Test case 35).
{"s3:PutObject", getReadWriteObjectStatement(bucketName, objectPrefix), true},
// write-only object policy is expected to allow PutObject operation on an anonymous request (Test case 36).
{"s3:PutObject", getWriteOnlyObjectStatement(bucketName, objectPrefix), true},
// s3:GetObject is the action necessary to be present in the bucket policy to allow
// downloading of an object for an Anonymous/unsigned request.
// Cases for testing GetObject access for different Bucket level access permissions (Test cases 37-39).
// Any of the Bucket level access permissions shouldn't allow for GetObject operation on an Anonymous/unsigned request.
{"s3:GetObject", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
{"s3:GetObject", getReadWriteBucketStatement(bucketName, objectPrefix), false},
{"s3:GetObject", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
// Cases for testing GetObject access for different Object level access permissions.
// read-only bucket policy is expected to allow downloading of an Object on an anonymous request (Test case 40).
{"s3:GetObject", getReadOnlyObjectStatement(bucketName, objectPrefix), true},
// read-write bucket policy is expected to allow downloading of an Object on an anonymous request (Test case 41).
{"s3:GetObject", getReadWriteObjectStatement(bucketName, objectPrefix), true},
// write-only bucket policy is expected to not allow downloading of an Object on an anonymous request (Test case 42).
{"s3:GetObject", getWriteOnlyObjectStatement(bucketName, objectPrefix), false},
// s3:ListMultipartUploadParts is the action necessary to be present in the bucket policy to allow
// Listing of uploaded parts for an Anonymous/unsigned request.
// Any of the Bucket level access permissions shouldn't allow for ListMultipartUploadParts operation on an Anonymous/unsigned request.
// read-only bucket policy is expected to not allow ListMultipartUploadParts operation on an anonymous request (Test cases 43-45).
{"s3:ListMultipartUploadParts", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
{"s3:ListMultipartUploadParts", getReadWriteBucketStatement(bucketName, objectPrefix), false},
{"s3:ListMultipartUploadParts", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
// read-only object policy is expected to not allow ListMultipartUploadParts operation on an anonymous request (Test case 46).
{"s3:ListMultipartUploadParts", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
// read-write object policy is expected to allow ListMultipartUploadParts operation on an anonymous request (Test case 47).
{"s3:ListMultipartUploadParts", getReadWriteObjectStatement(bucketName, objectPrefix), true},
// write-only object policy is expected to allow ListMultipartUploadParts operation on an anonymous request (Test case 48).
{"s3:ListMultipartUploadParts", getWriteOnlyObjectStatement(bucketName, objectPrefix), true},
} }
for i, testCase := range testCases { }
actualResult := bucketPolicyActionMatch(testCase.action, testCase.statement)
if testCase.expectedResult != actualResult { func getAnonReadOnlyObjectPolicy(bucketName, prefix string) *policy.Policy {
t.Errorf("Test %d: Expected the result to be `%v`, but instead found it to be `%v`", i+1, testCase.expectedResult, actualResult) return &policy.Policy{
} Version: policy.DefaultVersion,
Statements: []policy.Statement{policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(policy.GetObjectAction),
policy.NewResourceSet(policy.NewResource(bucketName, prefix)),
condition.NewFunctions(),
)},
}
}
func getAnonWriteOnlyObjectPolicy(bucketName, prefix string) *policy.Policy {
return &policy.Policy{
Version: policy.DefaultVersion,
Statements: []policy.Statement{policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(
policy.AbortMultipartUploadAction,
policy.DeleteObjectAction,
policy.ListMultipartUploadPartsAction,
policy.PutObjectAction,
),
policy.NewResourceSet(policy.NewResource(bucketName, prefix)),
condition.NewFunctions(),
)},
} }
} }
@ -290,7 +140,7 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
bucketName: bucketName, bucketName: bucketName,
bucketPolicyReader: bytes.NewReader([]byte(fmt.Sprintf(bucketPolicyTemplate, bucketName, bucketName))), bucketPolicyReader: bytes.NewReader([]byte(fmt.Sprintf(bucketPolicyTemplate, bucketName, bucketName))),
policyLen: maxAccessPolicySize + 1, policyLen: maxBucketPolicySize + 1,
accessKey: credentials.AccessKey, accessKey: credentials.AccessKey,
secretKey: credentials.SecretKey, secretKey: credentials.SecretKey,
expectedRespStatus: http.StatusBadRequest, expectedRespStatus: http.StatusBadRequest,
@ -430,7 +280,7 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse, // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again. // unsigned request goes through and its validated again.
ExecObjectLayerAPIAnonTest(t, obj, "PutBucketPolicyHandler", bucketName, "", instanceType, apiRouter, anonReq, getWriteOnlyObjectStatement) ExecObjectLayerAPIAnonTest(t, obj, "PutBucketPolicyHandler", bucketName, "", instanceType, apiRouter, anonReq, getAnonWriteOnlyBucketPolicy(bucketName))
// HTTP request for testing when `objectLayer` is set to `nil`. // HTTP request for testing when `objectLayer` is set to `nil`.
// There is no need to use an existing bucket and valid input for creating the request // There is no need to use an existing bucket and valid input for creating the request
@ -459,7 +309,7 @@ func TestGetBucketPolicyHandler(t *testing.T) {
func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler, func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
credentials auth.Credentials, t *testing.T) { credentials auth.Credentials, t *testing.T) {
// template for constructing HTTP request body for PUT bucket policy. // template for constructing HTTP request body for PUT bucket policy.
bucketPolicyTemplate := `{"Version":"2012-10-17","Statement":[{"Action":["s3:GetBucketLocation","s3:ListBucket"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s"],"Sid":""},{"Action":["s3:GetObject"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s/this*"],"Sid":""}]}` bucketPolicyTemplate := `{"Version":"2012-10-17","Statement":[{"Action":["s3:GetBucketLocation","s3:ListBucket"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s"]},{"Action":["s3:GetObject"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s/this*"]}]}`
// Writing bucket policy before running test on GetBucketPolicy. // Writing bucket policy before running test on GetBucketPolicy.
putTestPolicies := []struct { putTestPolicies := []struct {
@ -572,7 +422,16 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
if recV4.Code != testCase.expectedRespStatus { if recV4.Code != testCase.expectedRespStatus {
// Verify whether the bucket policy fetched is same as the one inserted. // Verify whether the bucket policy fetched is same as the one inserted.
if expectedBucketPolicyStr != string(bucketPolicyReadBuf) { expectedPolicy, err := policy.ParseConfig(strings.NewReader(expectedBucketPolicyStr), testCase.bucketName)
if err != nil {
t.Fatalf("unexpected error. %v", err)
}
gotPolicy, err := policy.ParseConfig(bytes.NewReader(bucketPolicyReadBuf), testCase.bucketName)
if err != nil {
t.Fatalf("unexpected error. %v", err)
}
if !reflect.DeepEqual(expectedPolicy, gotPolicy) {
t.Errorf("Test %d: %s: Bucket policy differs from expected value.", i+1, instanceType) t.Errorf("Test %d: %s: Bucket policy differs from expected value.", i+1, instanceType)
} }
} }
@ -598,7 +457,16 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
} }
if recV2.Code == http.StatusOK { if recV2.Code == http.StatusOK {
// Verify whether the bucket policy fetched is same as the one inserted. // Verify whether the bucket policy fetched is same as the one inserted.
if expectedBucketPolicyStr != string(bucketPolicyReadBuf) { expectedPolicy, err := policy.ParseConfig(strings.NewReader(expectedBucketPolicyStr), testCase.bucketName)
if err != nil {
t.Fatalf("unexpected error. %v", err)
}
gotPolicy, err := policy.ParseConfig(bytes.NewReader(bucketPolicyReadBuf), testCase.bucketName)
if err != nil {
t.Fatalf("unexpected error. %v", err)
}
if !reflect.DeepEqual(expectedPolicy, gotPolicy) {
t.Errorf("Test %d: %s: Bucket policy differs from expected value.", i+1, instanceType) t.Errorf("Test %d: %s: Bucket policy differs from expected value.", i+1, instanceType)
} }
} }
@ -617,7 +485,7 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse, // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again. // unsigned request goes through and its validated again.
ExecObjectLayerAPIAnonTest(t, obj, "GetBucketPolicyHandler", bucketName, "", instanceType, apiRouter, anonReq, getReadOnlyObjectStatement) ExecObjectLayerAPIAnonTest(t, obj, "GetBucketPolicyHandler", bucketName, "", instanceType, apiRouter, anonReq, getAnonReadOnlyBucketPolicy(bucketName))
// HTTP request for testing when `objectLayer` is set to `nil`. // HTTP request for testing when `objectLayer` is set to `nil`.
// There is no need to use an existing bucket and valid input for creating the request // There is no need to use an existing bucket and valid input for creating the request
@ -820,7 +688,7 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse, // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again. // unsigned request goes through and its validated again.
ExecObjectLayerAPIAnonTest(t, obj, "DeleteBucketPolicyHandler", bucketName, "", instanceType, apiRouter, anonReq, getReadOnlyObjectStatement) ExecObjectLayerAPIAnonTest(t, obj, "DeleteBucketPolicyHandler", bucketName, "", instanceType, apiRouter, anonReq, getAnonWriteOnlyBucketPolicy(bucketName))
// HTTP request for testing when `objectLayer` is set to `nil`. // HTTP request for testing when `objectLayer` is set to `nil`.
// There is no need to use an existing bucket and valid input for creating the request // There is no need to use an existing bucket and valid input for creating the request
@ -838,174 +706,3 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
// `ExecObjectLayerAPINilTest` manages the operation. // `ExecObjectLayerAPINilTest` manages the operation.
ExecObjectLayerAPINilTest(t, nilBucket, "", instanceType, apiRouter, nilReq) ExecObjectLayerAPINilTest(t, nilBucket, "", instanceType, apiRouter, nilReq)
} }
// TestBucketPolicyConditionMatch - Tests to validate whether bucket policy conditions match.
func TestBucketPolicyConditionMatch(t *testing.T) {
// obtain the inner map[string]set.StringSet for policy.Statement.Conditions.
getInnerMap := func(key2, value string) map[string]set.StringSet {
innerMap := make(map[string]set.StringSet)
innerMap[key2] = set.CreateStringSet(value)
return innerMap
}
// obtain policy.Statement with Conditions set.
getStatementWithCondition := func(key1, key2, value string) policy.Statement {
innerMap := getInnerMap(key2, value)
// to set policyStatment.Conditions .
conditions := make(policy.ConditionMap)
conditions[key1] = innerMap
// new policy statement.
statement := policy.Statement{}
// set the condition.
statement.Conditions = conditions
return statement
}
testCases := []struct {
statementCondition policy.Statement
condition map[string]set.StringSet
expectedMatch bool
}{
// Test case - 1.
// StringEquals condition matches.
{
statementCondition: getStatementWithCondition("StringEquals", "s3:prefix", "Asia/"),
condition: getInnerMap("prefix", "Asia/"),
expectedMatch: true,
},
// Test case - 2.
// StringEquals condition doesn't match.
{
statementCondition: getStatementWithCondition("StringEquals", "s3:prefix", "Asia/"),
condition: getInnerMap("prefix", "Africa/"),
expectedMatch: false,
},
// Test case - 3.
// StringEquals condition matches.
{
statementCondition: getStatementWithCondition("StringEquals", "s3:max-keys", "Asia/"),
condition: getInnerMap("max-keys", "Asia/"),
expectedMatch: true,
},
// Test case - 4.
// StringEquals condition doesn't match.
{
statementCondition: getStatementWithCondition("StringEquals", "s3:max-keys", "Asia/"),
condition: getInnerMap("max-keys", "Africa/"),
expectedMatch: false,
},
// Test case - 5.
// StringNotEquals condition matches.
{
statementCondition: getStatementWithCondition("StringNotEquals", "s3:prefix", "Asia/"),
condition: getInnerMap("prefix", "Asia/"),
expectedMatch: false,
},
// Test case - 6.
// StringNotEquals condition doesn't match.
{
statementCondition: getStatementWithCondition("StringNotEquals", "s3:prefix", "Asia/"),
condition: getInnerMap("prefix", "Africa/"),
expectedMatch: true,
},
// Test case - 7.
// StringNotEquals condition matches.
{
statementCondition: getStatementWithCondition("StringNotEquals", "s3:max-keys", "Asia/"),
condition: getInnerMap("max-keys", "Asia/"),
expectedMatch: false,
},
// Test case - 8.
// StringNotEquals condition doesn't match.
{
statementCondition: getStatementWithCondition("StringNotEquals", "s3:max-keys", "Asia/"),
condition: getInnerMap("max-keys", "Africa/"),
expectedMatch: true,
},
// Test case - 9.
// StringLike condition matches.
{
statementCondition: getStatementWithCondition("StringLike", "aws:Referer", "http://www.example.com/"),
condition: getInnerMap("referer", "http://www.example.com/"),
expectedMatch: true,
},
// Test case - 10.
// StringLike condition doesn't match.
{
statementCondition: getStatementWithCondition("StringLike", "aws:Referer", "http://www.example.com/"),
condition: getInnerMap("referer", "www.somethingelse.com"),
expectedMatch: false,
},
// Test case - 11.
// StringNotLike condition evaluates to false.
{
statementCondition: getStatementWithCondition("StringNotLike", "aws:Referer", "http://www.example.com/"),
condition: getInnerMap("referer", "http://www.example.com/"),
expectedMatch: false,
},
// Test case - 12.
// StringNotLike condition evaluates to true.
{
statementCondition: getStatementWithCondition("StringNotLike", "aws:Referer", "http://www.example.com/"),
condition: getInnerMap("referer", "http://somethingelse.com/"),
expectedMatch: true,
},
// Test case 13.
// IpAddress condition evaluates to true.
{
statementCondition: getStatementWithCondition("IpAddress", "aws:SourceIp", "54.240.143.0/24"),
condition: getInnerMap("ip", "54.240.143.2"),
expectedMatch: true,
},
// Test case 14.
// IpAddress condition evaluates to false.
{
statementCondition: getStatementWithCondition("IpAddress", "aws:SourceIp", "54.240.143.0/24"),
condition: getInnerMap("ip", "127.240.143.224"),
expectedMatch: false,
},
// Test case 15.
// NotIpAddress condition evaluates to true.
{
statementCondition: getStatementWithCondition("NotIpAddress", "aws:SourceIp", "54.240.143.0/24"),
condition: getInnerMap("ip", "54.240.144.188"),
expectedMatch: true,
},
// Test case 16.
// NotIpAddress condition evaluates to false.
{
statementCondition: getStatementWithCondition("NotIpAddress", "aws:SourceIp", "54.240.143.0/24"),
condition: getInnerMap("ip", "54.240.143.243"),
expectedMatch: false,
},
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("Case %d", i+1), func(t *testing.T) {
// call the function under test and assert the result with the expected result.
doesMatch := bucketPolicyConditionMatch(tc.condition, tc.statementCondition)
if tc.expectedMatch != doesMatch {
t.Errorf("Expected the match to be `%v`; got `%v` - %v %v.",
tc.expectedMatch, doesMatch, tc.condition, tc.statementCondition)
}
})
}
}

View File

@ -1,295 +0,0 @@
/*
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Package cmd This file implements AWS Access Policy Language parser in
// accordance with http://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
package cmd
import (
"encoding/json"
"errors"
"fmt"
"io"
"sort"
"strings"
"github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio-go/pkg/set"
)
var emptyBucketPolicy = policy.BucketAccessPolicy{}
var conditionKeyActionMap = policy.ConditionKeyMap{
"s3:prefix": set.CreateStringSet("s3:ListBucket", "s3:ListBucketMultipartUploads"),
"s3:max-keys": set.CreateStringSet("s3:ListBucket", "s3:ListBucketMultipartUploads",
"s3:ListMultipartUploadParts"),
}
// supportedActionMap - lists all the actions supported by minio.
var supportedActionMap = set.CreateStringSet("*", "s3:*", "s3:GetObject",
"s3:ListBucket", "s3:PutObject", "s3:GetBucketLocation", "s3:DeleteObject",
"s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts")
// supported Conditions type.
var supportedConditionsType = set.CreateStringSet("StringEquals", "StringNotEquals", "StringLike", "StringNotLike", "IpAddress", "NotIpAddress")
// Validate s3:prefix, s3:max-keys are present if not
// supported keys for the conditions.
var supportedConditionsKey = set.CreateStringSet("s3:prefix", "s3:max-keys", "aws:Referer", "aws:SourceIp")
// supportedEffectMap - supported effects.
var supportedEffectMap = set.CreateStringSet("Allow", "Deny")
// isValidActions - are actions valid.
func isValidActions(actions set.StringSet) (err error) {
// Statement actions cannot be empty.
if actions.IsEmpty() {
err = errors.New("Action list cannot be empty")
return err
}
if unsupportedActions := actions.Difference(supportedActionMap); !unsupportedActions.IsEmpty() {
err = fmt.Errorf("Unsupported actions found: %#v, please validate your policy document",
unsupportedActions)
return err
}
return nil
}
// isValidEffect - is effect valid.
func isValidEffect(effect string) (err error) {
// Statement effect cannot be empty.
if effect == "" {
err = errors.New("Policy effect cannot be empty")
return err
}
if !supportedEffectMap.Contains(effect) {
err = errors.New("Unsupported Effect found: " + effect + ", please validate your policy document")
return err
}
return nil
}
// isValidResources - are valid resources.
func isValidResources(resources set.StringSet) (err error) {
// Statement resources cannot be empty.
if resources.IsEmpty() {
err = errors.New("Resource list cannot be empty")
return err
}
for resource := range resources {
if !hasPrefix(resource, bucketARNPrefix) {
err = errors.New("Unsupported resource style found: " + resource + ", please validate your policy document")
return err
}
resourceSuffix := strings.SplitAfter(resource, bucketARNPrefix)[1]
if len(resourceSuffix) == 0 || hasPrefix(resourceSuffix, "/") {
err = errors.New("Invalid resource style found: " + resource + ", please validate your policy document")
return err
}
}
return nil
}
// isValidPrincipals - are valid principals.
func isValidPrincipals(principal policy.User) (err error) {
if principal.AWS.IsEmpty() {
return errors.New("Principal cannot be empty")
}
if diff := principal.AWS.Difference(set.CreateStringSet("*")); !diff.IsEmpty() {
// Minio does not support or implement IAM, "*" is the only valid value.
// Amazon s3 doc on principal:
// http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Principal
err = fmt.Errorf("Unsupported principals found: %#v, please validate your policy document",
diff)
return err
}
return nil
}
// isValidConditions - returns nil if the given conditions valid and
// corresponding error otherwise.
func isValidConditions(actions set.StringSet, conditions policy.ConditionMap) (err error) {
// Verify conditions should be valid. Validate if only
// supported condition keys are present and return error
// otherwise.
conditionKeyVal := make(map[string]set.StringSet)
for conditionType := range conditions {
if !supportedConditionsType.Contains(conditionType) {
err = fmt.Errorf("Unsupported condition type '%s', please validate your policy document", conditionType)
return err
}
for key, value := range conditions[conditionType] {
if !supportedConditionsKey.Contains(key) {
err = fmt.Errorf("Unsupported condition key '%s', please validate your policy document", conditionType)
return err
}
compatibleActions := conditionKeyActionMap[key]
if !compatibleActions.IsEmpty() &&
compatibleActions.Intersection(actions).IsEmpty() {
err = fmt.Errorf("Unsupported condition key %s for the given actions %s, "+
"please validate your policy document", key, actions)
return err
}
conditionVal, ok := conditionKeyVal[key]
if ok && !value.Intersection(conditionVal).IsEmpty() {
err = fmt.Errorf("Ambigious condition values for key '%s', please validate your policy document", key)
return err
}
conditionKeyVal[key] = value
}
}
return nil
}
// List of actions for which prefixes are not allowed.
var invalidPrefixActions = set.StringSet{
"s3:GetBucketLocation": {},
"s3:ListBucket": {},
"s3:ListBucketMultipartUploads": {},
// Add actions which do not honor prefixes.
}
// resourcePrefix - provides the prefix removing any wildcards.
func resourcePrefix(resource string) string {
if strings.HasSuffix(resource, "*") {
resource = strings.TrimSuffix(resource, "*")
}
return resource
}
// checkBucketPolicyResources validates Resources in unmarshalled bucket policy structure.
// - Resources are validated against the given set of Actions.
// -
func checkBucketPolicyResources(bucket string, bucketPolicy policy.BucketAccessPolicy) APIErrorCode {
// Validate statements for special actions and collect resources
// for others to validate nesting.
var resourceMap = set.NewStringSet()
for _, statement := range bucketPolicy.Statements {
for action := range statement.Actions {
for resource := range statement.Resources {
resourcePrefix := strings.SplitAfter(resource, bucketARNPrefix)[1]
if _, ok := invalidPrefixActions[action]; ok {
// Resource prefix is not equal to bucket for
// prefix invalid actions, reject them.
if resourcePrefix != bucket {
return ErrMalformedPolicy
}
} else {
// For all other actions validate if resourcePrefix begins
// with bucket name, if not reject them.
if strings.Split(resourcePrefix, "/")[0] != bucket {
return ErrMalformedPolicy
}
// All valid resources collect them separately to verify nesting.
resourceMap.Add(resourcePrefix)
}
}
}
}
var resources []string
for resource := range resourceMap {
resources = append(resources, resourcePrefix(resource))
}
// Sort strings as shorter first.
sort.Strings(resources)
for len(resources) > 1 {
var resource string
resource, resources = resources[0], resources[1:]
// Loop through all resources, if one of them matches with
// previous shorter one, it means we have detected
// nesting. Reject such rules.
for _, otherResource := range resources {
// Common prefix reject such rules.
if hasPrefix(otherResource, resource) {
return ErrPolicyNesting
}
}
}
// No errors found.
return ErrNone
}
// parseBucketPolicy - parses and validates if bucket policy is of
// proper JSON and follows allowed restrictions with policy standards.
func parseBucketPolicy(bucketPolicyReader io.Reader, bktPolicy *policy.BucketAccessPolicy) (err error) {
// Parse bucket policy reader.
decoder := json.NewDecoder(bucketPolicyReader)
if err = decoder.Decode(bktPolicy); err != nil {
return err
}
// Policy version cannot be empty.
if len(bktPolicy.Version) == 0 {
err = errors.New("Policy version cannot be empty")
return err
}
// Policy statements cannot be empty.
if len(bktPolicy.Statements) == 0 {
err = errors.New("Policy statement cannot be empty")
return err
}
// Loop through all policy statements and validate entries.
for _, statement := range bktPolicy.Statements {
// Statement effect should be valid.
if err := isValidEffect(statement.Effect); err != nil {
return err
}
// Statement principal should be supported format.
if err := isValidPrincipals(statement.Principal); err != nil {
return err
}
// Statement actions should be valid.
if err := isValidActions(statement.Actions); err != nil {
return err
}
// Statement resources should be valid.
if err := isValidResources(statement.Resources); err != nil {
return err
}
// Statement conditions should be valid.
if err := isValidConditions(statement.Actions, statement.Conditions); err != nil {
return err
}
}
// Separate deny and allow statements, so that we can apply deny
// statements in the beginning followed by Allow statements.
var denyStatements []policy.Statement
var allowStatements []policy.Statement
for _, statement := range bktPolicy.Statements {
if statement.Effect == "Deny" {
denyStatements = append(denyStatements, statement)
continue
}
// else if statement.Effect == "Allow"
allowStatements = append(allowStatements, statement)
}
// Deny statements are enforced first once matched.
bktPolicy.Statements = append(denyStatements, allowStatements...)
// Return successfully parsed policy structure.
return nil
}

View File

@ -1,867 +0,0 @@
/*
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"reflect"
"testing"
"github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio-go/pkg/set"
)
// Common bucket actions for both read and write policies.
var (
readWriteBucketActions = []string{
"s3:GetBucketLocation",
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
// Add more bucket level read-write actions here.
}
readWriteObjectActions = []string{
"s3:AbortMultipartUpload",
"s3:DeleteObject",
"s3:GetObject",
"s3:ListMultipartUploadParts",
"s3:PutObject",
// Add more object level read-write actions here.
}
)
// Write only actions.
var (
writeOnlyBucketActions = []string{
"s3:GetBucketLocation",
"s3:ListBucketMultipartUploads",
// Add more bucket level write actions here.
}
writeOnlyObjectActions = []string{
"s3:AbortMultipartUpload",
"s3:DeleteObject",
"s3:ListMultipartUploadParts",
"s3:PutObject",
// Add more object level write actions here.
}
)
// Read only actions.
var (
readOnlyBucketActions = []string{
"s3:GetBucketLocation",
"s3:ListBucket",
// Add more bucket level read actions here.
}
readOnlyObjectActions = []string{
"s3:GetObject",
// Add more object level read actions here.
}
)
// Obtain bucket statement for read-write bucketPolicy.
func getReadWriteObjectStatement(bucketName, objectPrefix string) policy.Statement {
objectResourceStatement := policy.Statement{}
objectResourceStatement.Effect = "Allow"
objectResourceStatement.Principal = policy.User{
AWS: set.StringSet{"*": struct{}{}},
}
objectResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName+"/"+objectPrefix+"*")}...)
objectResourceStatement.Actions = set.CreateStringSet(readWriteObjectActions...)
return objectResourceStatement
}
// Obtain object statement for read-write bucketPolicy.
func getReadWriteBucketStatement(bucketName, objectPrefix string) policy.Statement {
bucketResourceStatement := policy.Statement{}
bucketResourceStatement.Effect = "Allow"
bucketResourceStatement.Principal = policy.User{
AWS: set.StringSet{"*": struct{}{}},
}
bucketResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName)}...)
bucketResourceStatement.Actions = set.CreateStringSet(readWriteBucketActions...)
return bucketResourceStatement
}
// Obtain statements for read-write bucketPolicy.
func getReadWriteStatement(bucketName, objectPrefix string) []policy.Statement {
statements := []policy.Statement{}
// Save the read write policy.
statements = append(statements, getReadWriteBucketStatement(bucketName, objectPrefix), getReadWriteObjectStatement(bucketName, objectPrefix))
return statements
}
// Obtain bucket statement for read only bucketPolicy.
func getReadOnlyBucketStatement(bucketName, objectPrefix string) policy.Statement {
bucketResourceStatement := policy.Statement{}
bucketResourceStatement.Effect = "Allow"
bucketResourceStatement.Principal = policy.User{
AWS: set.StringSet{"*": struct{}{}},
}
bucketResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName)}...)
bucketResourceStatement.Actions = set.CreateStringSet(readOnlyBucketActions...)
return bucketResourceStatement
}
// Obtain object statement for read only bucketPolicy.
func getReadOnlyObjectStatement(bucketName, objectPrefix string) policy.Statement {
objectResourceStatement := policy.Statement{}
objectResourceStatement.Effect = "Allow"
objectResourceStatement.Principal = policy.User{
AWS: set.StringSet{"*": struct{}{}},
}
objectResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName+"/"+objectPrefix+"*")}...)
objectResourceStatement.Actions = set.CreateStringSet(readOnlyObjectActions...)
return objectResourceStatement
}
// Obtain statements for read only bucketPolicy.
func getReadOnlyStatement(bucketName, objectPrefix string) []policy.Statement {
statements := []policy.Statement{}
// Save the read only policy.
statements = append(statements, getReadOnlyBucketStatement(bucketName, objectPrefix), getReadOnlyObjectStatement(bucketName, objectPrefix))
return statements
}
// Obtain bucket statements for write only bucketPolicy.
func getWriteOnlyBucketStatement(bucketName, objectPrefix string) policy.Statement {
bucketResourceStatement := policy.Statement{}
bucketResourceStatement.Effect = "Allow"
bucketResourceStatement.Principal = policy.User{
AWS: set.StringSet{"*": struct{}{}},
}
bucketResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName)}...)
bucketResourceStatement.Actions = set.CreateStringSet(writeOnlyBucketActions...)
return bucketResourceStatement
}
// Obtain object statements for write only bucketPolicy.
func getWriteOnlyObjectStatement(bucketName, objectPrefix string) policy.Statement {
objectResourceStatement := policy.Statement{}
objectResourceStatement.Effect = "Allow"
objectResourceStatement.Principal = policy.User{
AWS: set.StringSet{"*": struct{}{}},
}
objectResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName+"/"+objectPrefix+"*")}...)
objectResourceStatement.Actions = set.CreateStringSet(writeOnlyObjectActions...)
return objectResourceStatement
}
// Obtain statements for write only bucketPolicy.
func getWriteOnlyStatement(bucketName, objectPrefix string) []policy.Statement {
statements := []policy.Statement{}
// Write only policy.
// Save the write only policy.
statements = append(statements, getWriteOnlyBucketStatement(bucketName, objectPrefix), getWriteOnlyBucketStatement(bucketName, objectPrefix))
return statements
}
// Tests validate Action validator.
func TestIsValidActions(t *testing.T) {
testCases := []struct {
// input.
actions set.StringSet
// expected output.
err error
// flag indicating whether the test should pass.
shouldPass bool
}{
// Inputs with unsupported Action.
// Test case - 1.
// "s3:ListObject" is an invalid Action.
{set.CreateStringSet([]string{"s3:GetObject", "s3:ListObject", "s3:RemoveObject"}...),
errors.New("Unsupported actions found: set.StringSet{\"s3:RemoveObject\":struct {}{}, \"s3:ListObject\":struct {}{}}, please validate your policy document"), false},
// Test case - 2.
// Empty Actions.
{set.CreateStringSet([]string{}...), errors.New("Action list cannot be empty"), false},
// Test case - 3.
// "s3:DeleteEverything"" is an invalid Action.
{set.CreateStringSet([]string{"s3:GetObject", "s3:ListBucket", "s3:PutObject", "s3:DeleteEverything"}...),
errors.New("Unsupported actions found: set.StringSet{\"s3:DeleteEverything\":struct {}{}}, please validate your policy document"), false},
// Inputs with valid Action.
// Test Case - 4.
{set.CreateStringSet([]string{
"s3:*", "*", "s3:GetObject", "s3:ListBucket",
"s3:PutObject", "s3:GetBucketLocation", "s3:DeleteObject",
"s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads",
"s3:ListMultipartUploadParts"}...), nil, true},
}
for i, testCase := range testCases {
err := isValidActions(testCase.actions)
if err != nil && testCase.shouldPass {
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error())
}
if err == nil && !testCase.shouldPass {
t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.err.Error())
}
}
}
// Tests validate Effect validator.
func TestIsValidEffect(t *testing.T) {
testCases := []struct {
// input.
effect string
// expected output.
err error
// flag indicating whether the test should pass.
shouldPass bool
}{
// Inputs with unsupported Effect.
// Test case - 1.
{"", errors.New("Policy effect cannot be empty"), false},
// Test case - 2.
{"DontAllow", errors.New("Unsupported Effect found: DontAllow, please validate your policy document"), false},
// Test case - 3.
{"NeverAllow", errors.New("Unsupported Effect found: NeverAllow, please validate your policy document"), false},
// Test case - 4.
{"AllowAlways", errors.New("Unsupported Effect found: AllowAlways, please validate your policy document"), false},
// Inputs with valid Effect.
// Test Case - 5.
{"Allow", nil, true},
// Test Case - 6.
{"Deny", nil, true},
}
for i, testCase := range testCases {
err := isValidEffect(testCase.effect)
if err != nil && testCase.shouldPass {
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error())
}
if err == nil && !testCase.shouldPass {
t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.err.Error())
}
// Failed as expected, but does it fail for the expected reason.
if err != nil && !testCase.shouldPass {
if err.Error() != testCase.err.Error() {
t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\"", i+1, testCase.err.Error(), err.Error())
}
}
}
}
// Tests validate Resources validator.
func TestIsValidResources(t *testing.T) {
testCases := []struct {
// input.
resources []string
// expected output.
err error
// flag indicating whether the test should pass.
shouldPass bool
}{
// Inputs with unsupported Action.
// Test case - 1.
// Empty Resources.
{[]string{}, errors.New("Resource list cannot be empty"), false},
// Test case - 2.
// A valid resource should have prefix bucketARNPrefix.
{[]string{"my-resource"}, errors.New("Unsupported resource style found: my-resource, please validate your policy document"), false},
// Test case - 3.
// A Valid resource should have bucket name followed by bucketARNPrefix.
{[]string{bucketARNPrefix}, errors.New("Invalid resource style found: arn:aws:s3:::, please validate your policy document"), false},
// Test Case - 4.
// Valid resource shouldn't have slash('/') followed by bucketARNPrefix.
{[]string{bucketARNPrefix + "/"}, errors.New("Invalid resource style found: arn:aws:s3:::/, please validate your policy document"), false},
// Test cases with valid Resources.
{[]string{bucketARNPrefix + "my-bucket"}, nil, true},
{[]string{bucketARNPrefix + "my-bucket/Asia/*"}, nil, true},
{[]string{bucketARNPrefix + "my-bucket/Asia/India/*"}, nil, true},
}
for i, testCase := range testCases {
err := isValidResources(set.CreateStringSet(testCase.resources...))
if err != nil && testCase.shouldPass {
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error())
}
if err == nil && !testCase.shouldPass {
t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.err.Error())
}
// Failed as expected, but does it fail for the expected reason.
if err != nil && !testCase.shouldPass {
if err.Error() != testCase.err.Error() {
t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\"", i+1, testCase.err.Error(), err.Error())
}
}
}
}
// Tests validate principals validator.
func TestIsValidPrincipals(t *testing.T) {
testCases := []struct {
// input.
principals []string
// expected output.
err error
// flag indicating whether the test should pass.
shouldPass bool
}{
// Inputs with unsupported Principals.
// Test case - 1.
// Empty Principals list.
{[]string{}, errors.New("Principal cannot be empty"), false},
// Test case - 2.
// "*" is the only valid principal.
{[]string{"my-principal"}, errors.New("Unsupported principals found: set.StringSet{\"my-principal\":struct {}{}}, please validate your policy document"), false},
// Test case - 3.
{[]string{"*", "111122233"}, errors.New("Unsupported principals found: set.StringSet{\"111122233\":struct {}{}}, please validate your policy document"), false},
// Test case - 4.
// Test case with valid principal value.
{[]string{"*"}, nil, true},
}
for i, testCase := range testCases {
u := policy.User{
AWS: set.CreateStringSet(testCase.principals...),
}
err := isValidPrincipals(u)
if err != nil && testCase.shouldPass {
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error())
}
if err == nil && !testCase.shouldPass {
t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.err.Error())
}
// Failed as expected, but does it fail for the expected reason.
if err != nil && !testCase.shouldPass {
if err.Error() != testCase.err.Error() {
t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\"", i+1, testCase.err.Error(), err.Error())
}
}
}
}
// getEmptyConditionMap - returns a function that generates a
// condition key map for a given key.
func getEmptyConditionMap(conditionKey string) func() policy.ConditionMap {
emptyConditonGenerator := func() policy.ConditionMap {
emptyMap := make(policy.ConditionKeyMap)
conditions := make(policy.ConditionMap)
conditions[conditionKey] = emptyMap
return conditions
}
return emptyConditonGenerator
}
// Tests validate policy.Statement condition validator.
func TestIsValidConditions(t *testing.T) {
// returns empty conditions map.
setEmptyConditions := func() policy.ConditionMap {
return make(policy.ConditionMap)
}
// returns map with the "StringEquals" set to empty map.
setEmptyStringEquals := getEmptyConditionMap("StringEquals")
// returns map with the "StringNotEquals" set to empty map.
setEmptyStringNotEquals := getEmptyConditionMap("StringNotEquals")
// returns map with the "StringLike" set to empty map.
setEmptyStringLike := getEmptyConditionMap("StringLike")
// returns map with the "StringNotLike" set to empty map.
setEmptyStringNotLike := getEmptyConditionMap("StringNotLike")
// returns map with the "IpAddress" set to empty map.
setEmptyIPAddress := getEmptyConditionMap("IpAddress")
// returns map with "NotIpAddress" set to empty map.
setEmptyNotIPAddress := getEmptyConditionMap("NotIpAddress")
// Generate conditions.
generateConditions := func(key1, key2, value string) policy.ConditionMap {
innerMap := make(policy.ConditionKeyMap)
innerMap[key2] = set.CreateStringSet(value)
conditions := make(policy.ConditionMap)
conditions[key1] = innerMap
return conditions
}
// generate ambigious conditions.
generateAmbigiousConditions := func() policy.ConditionMap {
prefixMap := make(policy.ConditionKeyMap)
prefixMap["s3:prefix"] = set.CreateStringSet("Asia/")
conditions := make(policy.ConditionMap)
conditions["StringEquals"] = prefixMap
conditions["StringNotEquals"] = prefixMap
return conditions
}
// generate valid and non valid type in the condition map.
generateValidInvalidConditions := func() policy.ConditionMap {
innerMap := make(policy.ConditionKeyMap)
innerMap["s3:prefix"] = set.CreateStringSet("Asia/")
conditions := make(policy.ConditionMap)
conditions["StringEquals"] = innerMap
conditions["InvalidType"] = innerMap
return conditions
}
// generate valid and invalid keys for valid types in the same condition map.
generateValidInvalidConditionKeys := func() policy.ConditionMap {
innerMapValid := make(policy.ConditionKeyMap)
innerMapValid["s3:prefix"] = set.CreateStringSet("Asia/")
innerMapInValid := make(map[string]set.StringSet)
innerMapInValid["s3:invalid"] = set.CreateStringSet("Asia/")
conditions := make(policy.ConditionMap)
conditions["StringEquals"] = innerMapValid
conditions["StringEquals"] = innerMapInValid
return conditions
}
// List of Conditions used for test cases.
testConditions := []policy.ConditionMap{
generateConditions("StringValues", "s3:max-keys", "100"),
generateConditions("StringEquals", "s3:Object", "100"),
generateAmbigiousConditions(),
generateValidInvalidConditions(),
generateValidInvalidConditionKeys(),
setEmptyConditions(),
setEmptyStringEquals(),
setEmptyStringNotEquals(),
setEmptyStringLike(),
setEmptyStringNotLike(),
setEmptyIPAddress(),
setEmptyNotIPAddress(),
generateConditions("StringEquals", "s3:prefix", "Asia/"),
generateConditions("StringEquals", "s3:max-keys", "100"),
generateConditions("StringNotEquals", "s3:prefix", "Asia/"),
generateConditions("StringNotEquals", "s3:max-keys", "100"),
}
getObjectActionSet := set.CreateStringSet("s3:GetObject")
roBucketActionSet := set.CreateStringSet(readOnlyBucketActions...)
maxKeysConditionErr := fmt.Errorf("Unsupported condition key %s for the given actions %s, "+
"please validate your policy document", "s3:max-keys", getObjectActionSet)
testCases := []struct {
inputActions set.StringSet
inputCondition policy.ConditionMap
// expected result.
expectedErr error
// flag indicating whether test should pass.
shouldPass bool
}{
// Malformed conditions.
// Test case - 1.
// "StringValues" is an invalid type.
{roBucketActionSet, testConditions[0], fmt.Errorf("Unsupported condition type 'StringValues', " +
"please validate your policy document"), false},
// Test case - 2.
// "s3:Object" is an invalid key.
{roBucketActionSet, testConditions[1], fmt.Errorf("Unsupported condition key " +
"'StringEquals', please validate your policy document"), false},
// Test case - 3.
// Test case with Ambigious conditions set.
{roBucketActionSet, testConditions[2], fmt.Errorf("Ambigious condition values for key 's3:prefix', " +
"please validate your policy document"), false},
// Test case - 4.
// Test case with valid and invalid condition types.
{roBucketActionSet, testConditions[3], fmt.Errorf("Unsupported condition type 'InvalidType', " +
"please validate your policy document"), false},
// Test case - 5.
// Test case with valid and invalid condition keys.
{roBucketActionSet, testConditions[4], fmt.Errorf("Unsupported condition key 'StringEquals', " +
"please validate your policy document"), false},
// Test cases with valid conditions.
// Test case - 6.
{roBucketActionSet, testConditions[5], nil, true},
// Test case - 7.
{roBucketActionSet, testConditions[6], nil, true},
// Test case - 8.
{roBucketActionSet, testConditions[7], nil, true},
// Test case - 9.
{roBucketActionSet, testConditions[8], nil, true},
// Test case - 10.
{roBucketActionSet, testConditions[9], nil, true},
// Test case - 11.
{roBucketActionSet, testConditions[10], nil, true},
// Test case - 12.
{roBucketActionSet, testConditions[11], nil, true},
// Test case - 13.
{roBucketActionSet, testConditions[12], nil, true},
// Test case - 11.
{roBucketActionSet, testConditions[13], nil, true},
// Test case - 12.
{roBucketActionSet, testConditions[14], nil, true},
// Test case - 13.
{getObjectActionSet, testConditions[15], maxKeysConditionErr, false},
}
for i, testCase := range testCases {
actualErr := isValidConditions(testCase.inputActions, testCase.inputCondition)
if actualErr != nil && testCase.shouldPass {
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, actualErr.Error())
}
if actualErr == nil && !testCase.shouldPass {
t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.expectedErr.Error())
}
// Failed as expected, but does it fail for the expected reason.
if actualErr != nil && !testCase.shouldPass {
if actualErr.Error() != testCase.expectedErr.Error() {
t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\"", i+1, testCase.expectedErr.Error(), actualErr.Error())
}
}
}
}
// Tests validate Policy Action and Resource fields.
func TestCheckbucketPolicyResources(t *testing.T) {
// constructing policy statement without invalidPrefixActions (check bucket-policy-parser.go).
setValidPrefixActions := func(statements []policy.Statement) []policy.Statement {
statements[0].Actions = set.CreateStringSet([]string{"s3:DeleteObject", "s3:PutObject"}...)
return statements
}
// contracting policy statement with recursive resources.
// should result in ErrMalformedPolicy
setRecurseResource := func(statements []policy.Statement) []policy.Statement {
statements[0].Resources = set.CreateStringSet([]string{"arn:aws:s3:::minio-bucket/Asia/*", "arn:aws:s3:::minio-bucket/Asia/India/*"}...)
return statements
}
// constructing policy statement with lexically close characters.
// should not result in ErrMalformedPolicy
setResourceLexical := func(statements []policy.Statement) []policy.Statement {
statements[0].Resources = set.CreateStringSet([]string{"arn:aws:s3:::minio-bucket/op*", "arn:aws:s3:::minio-bucket/oo*"}...)
return statements
}
// List of bucketPolicy used for tests.
bucketAccessPolicies := []policy.BucketAccessPolicy{
// bucketPolicy - 1.
// Contains valid read only policy statement.
{Version: "1.0", Statements: getReadOnlyStatement("minio-bucket", "")},
// bucketPolicy - 2.
// Contains valid read-write only policy statement.
{Version: "1.0", Statements: getReadWriteStatement("minio-bucket", "Asia/")},
// bucketPolicy - 3.
// Contains valid write only policy statement.
{Version: "1.0", Statements: getWriteOnlyStatement("minio-bucket", "Asia/India/")},
// bucketPolicy - 4.
// Contains invalidPrefixActions.
// Since resourcePrefix is not to the bucket-name, it return ErrMalformedPolicy.
{Version: "1.0", Statements: getReadOnlyStatement("minio-bucket-fail", "Asia/India/")},
// bucketPolicy - 5.
// constructing policy statement without invalidPrefixActions (check bucket-policy-parser.go).
// but bucket part of the resource is not equal to the bucket name.
// this results in return of ErrMalformedPolicy.
{Version: "1.0", Statements: setValidPrefixActions(getWriteOnlyStatement("minio-bucket-fail", "Asia/India/"))},
// bucketPolicy - 6.
// contracting policy statement with recursive resources.
// should result in ErrMalformedPolicy
{Version: "1.0", Statements: setRecurseResource(setValidPrefixActions(getWriteOnlyStatement("minio-bucket", "")))},
// BucketPolciy - 7.
// constructing policy statement with non recursive but
// lexically close resources.
// should result in ErrNone.
{Version: "1.0", Statements: setResourceLexical(setValidPrefixActions(getWriteOnlyStatement("minio-bucket", "oo")))},
}
testCases := []struct {
inputPolicy policy.BucketAccessPolicy
// expected results.
apiErrCode APIErrorCode
// Flag indicating whether the test should pass.
shouldPass bool
}{
// Test case - 1.
{bucketAccessPolicies[0], ErrNone, true},
// Test case - 2.
{bucketAccessPolicies[1], ErrNone, true},
// Test case - 3.
{bucketAccessPolicies[2], ErrNone, true},
// Test case - 4.
// contains invalidPrefixActions (check bucket-policy-parser.go).
// Resource prefix will not be equal to the bucket name in this case.
{bucketAccessPolicies[3], ErrMalformedPolicy, false},
// Test case - 5.
// actions contain invalidPrefixActions (check bucket-policy-parser.go).
// Resource prefix bucket part is not equal to the bucket name in this case.
{bucketAccessPolicies[4], ErrMalformedPolicy, false},
// Test case - 6.
// contracting policy statement with recursive resources.
// should result in ErrPolicyNesting.
{bucketAccessPolicies[5], ErrPolicyNesting, false},
// Test case - 7.
// constructing policy statement with lexically close
// characters.
// should result in ErrNone.
{bucketAccessPolicies[6], ErrNone, true},
}
for i, testCase := range testCases {
apiErrCode := checkBucketPolicyResources("minio-bucket", testCase.inputPolicy)
if apiErrCode != ErrNone && testCase.shouldPass {
t.Errorf("Test %d: Expected to pass, but failed with Errocode %v", i+1, apiErrCode)
}
if apiErrCode == ErrNone && !testCase.shouldPass {
t.Errorf("Test %d: Expected to fail with ErrCode %v, but passed instead", i+1, testCase.apiErrCode)
}
// Failed as expected, but does it fail for the expected reason.
if apiErrCode != ErrNone && !testCase.shouldPass {
if testCase.apiErrCode != apiErrCode {
t.Errorf("Test %d: Expected to fail with error code %v, but instead failed with error code %v", i+1, testCase.apiErrCode, apiErrCode)
}
}
}
}
// Tests validate parsing of BucketAccessPolicy.
func TestParseBucketPolicy(t *testing.T) {
// set Unsupported Actions.
setUnsupportedActions := func(statements []policy.Statement) []policy.Statement {
// "s3:DeleteEverything"" is an Unsupported Action.
statements[0].Actions = set.CreateStringSet([]string{"s3:GetObject", "s3:ListBucket", "s3:PutObject", "s3:DeleteEverything"}...)
return statements
}
// set unsupported Effect.
setUnsupportedEffect := func(statements []policy.Statement) []policy.Statement {
// Effect "Don't allow" is Unsupported.
statements[0].Effect = "DontAllow"
return statements
}
// set unsupported principals.
setUnsupportedPrincipals := func(statements []policy.Statement) []policy.Statement {
// "User1111"" is an Unsupported Principal.
statements[0].Principal = policy.User{
AWS: set.CreateStringSet([]string{"*", "User1111"}...),
}
return statements
}
// set unsupported Resources.
setUnsupportedResources := func(statements []policy.Statement) []policy.Statement {
// "s3:DeleteEverything"" is an Unsupported Action.
statements[0].Resources = set.CreateStringSet([]string{"my-resource"}...)
return statements
}
// List of bucketPolicy used for test cases.
bucketAccesPolicies := []policy.BucketAccessPolicy{
// bucketPolicy - 0.
// bucketPolicy statement empty.
{Version: "1.0"},
// bucketPolicy - 1.
// bucketPolicy version empty.
{Version: "", Statements: []policy.Statement{}},
// bucketPolicy - 2.
// Readonly bucketPolicy.
{Version: "1.0", Statements: getReadOnlyStatement("minio-bucket", "")},
// bucketPolicy - 3.
// Read-Write bucket policy.
{Version: "1.0", Statements: getReadWriteStatement("minio-bucket", "Asia/")},
// bucketPolicy - 4.
// Write only bucket policy.
{Version: "1.0", Statements: getWriteOnlyStatement("minio-bucket", "Asia/India/")},
// bucketPolicy - 5.
// bucketPolicy statement contains unsupported action.
{Version: "1.0", Statements: setUnsupportedActions(getReadOnlyStatement("minio-bucket", ""))},
// bucketPolicy - 6.
// bucketPolicy statement contains unsupported Effect.
{Version: "1.0", Statements: setUnsupportedEffect(getReadWriteStatement("minio-bucket", "Asia/"))},
// bucketPolicy - 7.
// bucketPolicy statement contains unsupported Principal.
{Version: "1.0", Statements: setUnsupportedPrincipals(getWriteOnlyStatement("minio-bucket", "Asia/India/"))},
// bucketPolicy - 8.
// bucketPolicy statement contains unsupported Resource.
{Version: "1.0", Statements: setUnsupportedResources(getWriteOnlyStatement("minio-bucket", "Asia/India/"))},
}
testCases := []struct {
inputPolicy policy.BucketAccessPolicy
// expected results.
expectedPolicy policy.BucketAccessPolicy
err error
// Flag indicating whether the test should pass.
shouldPass bool
}{
// Test case - 1.
// bucketPolicy statement empty.
{bucketAccesPolicies[0], policy.BucketAccessPolicy{}, errors.New("Policy statement cannot be empty"), false},
// Test case - 2.
// bucketPolicy version empty.
{bucketAccesPolicies[1], policy.BucketAccessPolicy{}, errors.New("Policy version cannot be empty"), false},
// Test case - 3.
// Readonly bucketPolicy.
{bucketAccesPolicies[2], bucketAccesPolicies[2], nil, true},
// Test case - 4.
// Read-Write bucket policy.
{bucketAccesPolicies[3], bucketAccesPolicies[3], nil, true},
// Test case - 5.
// Write only bucket policy.
{bucketAccesPolicies[4], bucketAccesPolicies[4], nil, true},
// Test case - 6.
// bucketPolicy statement contains unsupported action.
{bucketAccesPolicies[5], bucketAccesPolicies[5], fmt.Errorf("Unsupported actions found: set.StringSet{\"s3:DeleteEverything\":struct {}{}}, please validate your policy document"), false},
// Test case - 7.
// bucketPolicy statement contains unsupported Effect.
{bucketAccesPolicies[6], bucketAccesPolicies[6], fmt.Errorf("Unsupported Effect found: DontAllow, please validate your policy document"), false},
// Test case - 8.
// bucketPolicy statement contains unsupported Principal.
{bucketAccesPolicies[7], bucketAccesPolicies[7], fmt.Errorf("Unsupported principals found: set.StringSet{\"User1111\":struct {}{}}, please validate your policy document"), false},
// Test case - 9.
// bucketPolicy statement contains unsupported Resource.
{bucketAccesPolicies[8], bucketAccesPolicies[8], fmt.Errorf("Unsupported resource style found: my-resource, please validate your policy document"), false},
}
for i, testCase := range testCases {
var buffer bytes.Buffer
encoder := json.NewEncoder(&buffer)
err := encoder.Encode(testCase.inputPolicy)
if err != nil {
t.Fatalf("Test %d: Couldn't Marshal bucket policy %s", i+1, err)
}
var actualAccessPolicy = policy.BucketAccessPolicy{}
err = parseBucketPolicy(&buffer, &actualAccessPolicy)
if err != nil && testCase.shouldPass {
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error())
}
if err == nil && !testCase.shouldPass {
t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.err.Error())
}
// Failed as expected, but does it fail for the expected reason.
if err != nil && !testCase.shouldPass {
if err.Error() != testCase.err.Error() {
t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\"", i+1, testCase.err.Error(), err.Error())
}
}
// Test passes as expected, but the output values are verified for correctness here.
if err == nil && testCase.shouldPass {
if !reflect.DeepEqual(testCase.expectedPolicy, actualAccessPolicy) {
t.Errorf("Test %d: The expected statements from resource statement generator doesn't match the actual statements", i+1)
}
}
}
}
func TestAWSRefererCondition(t *testing.T) {
resource := set.CreateStringSet([]string{
fmt.Sprintf("%s%s", bucketARNPrefix, "minio-bucket"+"/"+"Asia"+"*"),
}...)
conditionsKeyMap := make(policy.ConditionKeyMap)
conditionsKeyMap.Add("aws:Referer",
set.CreateStringSet("www.example.com",
"http://www.example.com"))
requestConditionMap := make(policy.ConditionKeyMap)
requestConditionMap["referer"] = set.CreateStringSet("www.example.com")
testCases := []struct {
effect string
conditionKey string
match bool
}{
{
effect: "Allow",
conditionKey: "StringLike",
match: true,
},
{
effect: "Allow",
conditionKey: "StringNotLike",
match: false,
},
{
effect: "Deny",
conditionKey: "StringLike",
match: true,
},
{
effect: "Deny",
conditionKey: "StringNotLike",
match: false,
},
}
for i, test := range testCases {
conditions := make(policy.ConditionMap)
conditions[test.conditionKey] = conditionsKeyMap
allowStatement := policy.Statement{
Sid: "Testing AWS referer condition",
Effect: test.effect,
Principal: policy.User{
AWS: set.CreateStringSet("*"),
},
Resources: resource,
Conditions: conditions,
}
if result := bucketPolicyConditionMatch(requestConditionMap, allowStatement); result != test.match {
t.Errorf("Test %d - Expected conditons to evaluate to %v but got %v",
i+1, test.match, result)
}
}
}
func TestAWSSourceIPCondition(t *testing.T) {
resource := set.CreateStringSet([]string{
fmt.Sprintf("%s%s", bucketARNPrefix, "minio-bucket"+"/"+"Asia"+"*"),
}...)
conditionsKeyMap := make(policy.ConditionKeyMap)
// Test both IPv4 and IPv6 addresses.
conditionsKeyMap.Add("aws:SourceIp",
set.CreateStringSet("54.240.143.0/24",
"2001:DB8:1234:5678::/64"))
requestConditionMap := make(policy.ConditionKeyMap)
requestConditionMap["ip"] = set.CreateStringSet("54.240.143.2")
testCases := []struct {
effect string
conditionKey string
match bool
}{
{
effect: "Allow",
conditionKey: "IpAddress",
match: true,
},
{
effect: "Allow",
conditionKey: "NotIpAddress",
match: false,
},
{
effect: "Deny",
conditionKey: "IpAddress",
match: true,
},
{
effect: "Deny",
conditionKey: "NotIpAddress",
match: false,
},
}
for i, test := range testCases {
conditions := make(policy.ConditionMap)
conditions[test.conditionKey] = conditionsKeyMap
allowStatement := policy.Statement{
Sid: "Testing AWS referer condition",
Effect: test.effect,
Principal: policy.User{
AWS: set.CreateStringSet("*"),
},
Resources: resource,
Conditions: conditions,
}
if result := bucketPolicyConditionMatch(requestConditionMap, allowStatement); result != test.match {
t.Errorf("Test %d - Expected conditons to evaluate to %v but got %v",
i+1, test.match, result)
}
}
}

View File

@ -1,206 +0,0 @@
/*
* Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"bytes"
"context"
"encoding/json"
"io"
"reflect"
"sync"
"github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/hash"
)
const (
// Static prefix to be used while constructing bucket ARN.
// refer to S3 docs for more info.
bucketARNPrefix = "arn:aws:s3:::"
// Bucket policy config name.
bucketPolicyConfig = "policy.json"
)
// Global bucket policies list, policies are enforced on each bucket looking
// through the policies here.
type bucketPolicies struct {
rwMutex *sync.RWMutex
// Collection of 'bucket' policies.
bucketPolicyConfigs map[string]policy.BucketAccessPolicy
}
// Fetch bucket policy for a given bucket.
func (bp bucketPolicies) GetBucketPolicy(bucket string) policy.BucketAccessPolicy {
bp.rwMutex.RLock()
defer bp.rwMutex.RUnlock()
return bp.bucketPolicyConfigs[bucket]
}
// Set a new bucket policy for a bucket, this operation will overwrite
// any previous bucket policies for the bucket.
func (bp *bucketPolicies) SetBucketPolicy(bucket string, newpolicy policy.BucketAccessPolicy) error {
bp.rwMutex.Lock()
defer bp.rwMutex.Unlock()
if reflect.DeepEqual(newpolicy, emptyBucketPolicy) {
return errInvalidArgument
}
bp.bucketPolicyConfigs[bucket] = newpolicy
return nil
}
// Delete bucket policy from struct for a given bucket.
func (bp *bucketPolicies) DeleteBucketPolicy(bucket string) error {
bp.rwMutex.Lock()
defer bp.rwMutex.Unlock()
delete(bp.bucketPolicyConfigs, bucket)
return nil
}
// Intialize all bucket policies.
func initBucketPolicies(objAPI ObjectLayer) (*bucketPolicies, error) {
if objAPI == nil {
return nil, errInvalidArgument
}
// List buckets to proceed loading all notification configuration.
buckets, err := objAPI.ListBuckets(context.Background())
if err != nil {
return nil, err
}
policies := make(map[string]policy.BucketAccessPolicy)
// Loads bucket policy.
for _, bucket := range buckets {
bp, pErr := ReadBucketPolicy(bucket.Name, objAPI)
if pErr != nil {
// net.Dial fails for rpc client or any
// other unexpected errors during net.Dial.
if !IsErrIgnored(pErr, errDiskNotFound) {
if !isErrBucketPolicyNotFound(pErr) {
return nil, pErr
}
}
// Continue to load other bucket policies if possible.
continue
}
policies[bucket.Name] = bp
}
// Return all bucket policies.
return &bucketPolicies{
rwMutex: &sync.RWMutex{},
bucketPolicyConfigs: policies,
}, nil
}
// readBucketPolicyJSON - reads bucket policy for an input bucket, returns BucketPolicyNotFound
// if bucket policy is not found.
func readBucketPolicyJSON(bucket string, objAPI ObjectLayer) (bucketPolicyReader io.Reader, err error) {
policyPath := pathJoin(bucketConfigPrefix, bucket, bucketPolicyConfig)
var buffer bytes.Buffer
ctx := logger.SetReqInfo(context.Background(), &logger.ReqInfo{BucketName: bucket})
err = objAPI.GetObject(ctx, minioMetaBucket, policyPath, 0, -1, &buffer, "")
if err != nil {
if isErrObjectNotFound(err) || isErrIncompleteBody(err) {
return nil, PolicyNotFound{Bucket: bucket}
}
return nil, err
}
return &buffer, nil
}
// ReadBucketPolicy - reads bucket policy for an input bucket, returns BucketPolicyNotFound
// if bucket policy is not found. This function also parses the bucket policy into an object.
func ReadBucketPolicy(bucket string, objAPI ObjectLayer) (policy.BucketAccessPolicy, error) {
// Read bucket policy JSON.
bucketPolicyReader, err := readBucketPolicyJSON(bucket, objAPI)
if err != nil {
return emptyBucketPolicy, err
}
// Parse the saved policy.
var bp policy.BucketAccessPolicy
if err = parseBucketPolicy(bucketPolicyReader, &bp); err != nil {
return emptyBucketPolicy, err
}
return bp, nil
}
// removeBucketPolicy - removes any previously written bucket policy. Returns BucketPolicyNotFound
// if no policies are found.
func removeBucketPolicy(ctx context.Context, bucket string, objAPI ObjectLayer) error {
policyPath := pathJoin(bucketConfigPrefix, bucket, bucketPolicyConfig)
err := objAPI.DeleteObject(ctx, minioMetaBucket, policyPath)
if err != nil {
if _, ok := err.(ObjectNotFound); ok {
return BucketPolicyNotFound{Bucket: bucket}
}
return err
}
return nil
}
// writeBucketPolicy - save a bucket policy that is assumed to be validated.
func writeBucketPolicy(ctx context.Context, bucket string, objAPI ObjectLayer, bpy policy.BucketAccessPolicy) error {
buf, err := json.Marshal(bpy)
if err != nil {
logger.LogIf(ctx, err)
return err
}
policyPath := pathJoin(bucketConfigPrefix, bucket, bucketPolicyConfig)
hashReader, err := hash.NewReader(bytes.NewReader(buf), int64(len(buf)), "", getSHA256Hash(buf))
if err != nil {
logger.LogIf(ctx, err)
return err
}
if _, err = objAPI.PutObject(ctx, minioMetaBucket, policyPath, hashReader, nil); err != nil {
return err
}
return nil
}
// persistAndNotifyBucketPolicyChange - takes a policyChange argument,
// persists it to storage, and notify nodes in the cluster about the
// change. In-memory state is updated in response to the notification.
func persistAndNotifyBucketPolicyChange(ctx context.Context, bucket string, isRemove bool, bktPolicy policy.BucketAccessPolicy, objAPI ObjectLayer) error {
if isRemove {
err := removeBucketPolicy(ctx, bucket, objAPI)
if err != nil {
return err
}
} else {
if reflect.DeepEqual(bktPolicy, emptyBucketPolicy) {
return errInvalidArgument
}
if err := writeBucketPolicy(ctx, bucket, objAPI, bktPolicy); err != nil {
return err
}
}
return nil
}

View File

@ -24,16 +24,15 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
"reflect"
"sort" "sort"
"sync" "sync"
"time" "time"
"github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/lock" "github.com/minio/minio/pkg/lock"
"github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/madmin"
"github.com/minio/minio/pkg/policy"
) )
// FSObjects - Implements fs object layer. // FSObjects - Implements fs object layer.
@ -60,9 +59,6 @@ type FSObjects struct {
// To manage the appendRoutine go-routines // To manage the appendRoutine go-routines
nsMutex *nsLockMap nsMutex *nsLockMap
// Variable represents bucket policies in memory.
bucketPolicies *bucketPolicies
} }
// Represents the background append file. // Represents the background append file.
@ -139,15 +135,14 @@ func NewFSObjectLayer(fsPath string) (ObjectLayer, error) {
// or cause changes on backend format. // or cause changes on backend format.
fs.fsFormatRlk = rlk fs.fsFormatRlk = rlk
// Initialize and load bucket policies.
fs.bucketPolicies, err = initBucketPolicies(fs)
if err != nil {
return nil, fmt.Errorf("Unable to load all bucket policies. %s", err)
}
// Initialize notification system. // Initialize notification system.
if err = globalNotificationSys.Init(fs); err != nil { if err = globalNotificationSys.Init(fs); err != nil {
return nil, fmt.Errorf("Unable to initialize event notification. %s", err) return nil, fmt.Errorf("Unable to initialize notification system. %v", err)
}
// Initialize policy system.
if err = globalPolicySys.Init(fs); err != nil {
return nil, fmt.Errorf("Unable to initialize policy system. %v", err)
} }
go fs.cleanupStaleMultipartUploads(ctx, globalMultipartCleanupInterval, globalMultipartExpiry, globalServiceDoneCh) go fs.cleanupStaleMultipartUploads(ctx, globalMultipartCleanupInterval, globalMultipartExpiry, globalServiceDoneCh)
@ -1058,22 +1053,18 @@ func (fs *FSObjects) ListBucketsHeal(ctx context.Context) ([]BucketInfo, error)
} }
// SetBucketPolicy sets policy on bucket // SetBucketPolicy sets policy on bucket
func (fs *FSObjects) SetBucketPolicy(ctx context.Context, bucket string, policy policy.BucketAccessPolicy) error { func (fs *FSObjects) SetBucketPolicy(ctx context.Context, bucket string, policy *policy.Policy) error {
return persistAndNotifyBucketPolicyChange(ctx, bucket, false, policy, fs) return savePolicyConfig(fs, bucket, policy)
} }
// GetBucketPolicy will get policy on bucket // GetBucketPolicy will get policy on bucket
func (fs *FSObjects) GetBucketPolicy(ctx context.Context, bucket string) (policy.BucketAccessPolicy, error) { func (fs *FSObjects) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
policy := fs.bucketPolicies.GetBucketPolicy(bucket) return GetPolicyConfig(fs, bucket)
if reflect.DeepEqual(policy, emptyBucketPolicy) {
return ReadBucketPolicy(bucket, fs)
}
return policy, nil
} }
// DeleteBucketPolicy deletes all policies on bucket // DeleteBucketPolicy deletes all policies on bucket
func (fs *FSObjects) DeleteBucketPolicy(ctx context.Context, bucket string) error { func (fs *FSObjects) DeleteBucketPolicy(ctx context.Context, bucket string) error {
return persistAndNotifyBucketPolicyChange(ctx, bucket, true, emptyBucketPolicy, fs) return removePolicyConfig(ctx, fs, bucket)
} }
// ListObjectsV2 lists all blobs in bucket filtered by prefix // ListObjectsV2 lists all blobs in bucket filtered by prefix
@ -1093,19 +1084,6 @@ func (fs *FSObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continua
return listObjectsV2Info, err return listObjectsV2Info, err
} }
// RefreshBucketPolicy refreshes cache policy with what's on disk.
func (fs *FSObjects) RefreshBucketPolicy(ctx context.Context, bucket string) error {
policy, err := ReadBucketPolicy(bucket, fs)
if err != nil {
if reflect.DeepEqual(policy, emptyBucketPolicy) {
return fs.bucketPolicies.DeleteBucketPolicy(bucket)
}
return err
}
return fs.bucketPolicies.SetBucketPolicy(bucket, policy)
}
// IsNotificationSupported returns whether bucket notification is applicable for this layer. // IsNotificationSupported returns whether bucket notification is applicable for this layer.
func (fs *FSObjects) IsNotificationSupported() bool { func (fs *FSObjects) IsNotificationSupported() bool {
return true return true

View File

@ -281,7 +281,7 @@ func ErrorRespToObjectError(err error, params ...string) error {
case "BucketNotEmpty": case "BucketNotEmpty":
err = BucketNotEmpty{} err = BucketNotEmpty{}
case "NoSuchBucketPolicy": case "NoSuchBucketPolicy":
err = PolicyNotFound{} err = BucketPolicyNotFound{}
case "InvalidBucketName": case "InvalidBucketName":
err = BucketNameInvalid{Bucket: bucket} err = BucketNameInvalid{Bucket: bucket}
case "NoSuchBucket": case "NoSuchBucket":

View File

@ -167,9 +167,12 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
initNSLock(false) // Enable local namespace lock. initNSLock(false) // Enable local namespace lock.
// Initialize notification system. // Create new notification system.
globalNotificationSys, err = NewNotificationSys(globalServerConfig, EndpointList{}) globalNotificationSys, err = NewNotificationSys(globalServerConfig, EndpointList{})
logger.FatalIf(err, "Unable to initialize notification system.") logger.FatalIf(err, "Unable to create new notification system.")
// Create new policy system.
globalPolicySys = NewPolicySys()
newObject, err := gw.NewGatewayLayer(globalServerConfig.GetCredential()) newObject, err := gw.NewGatewayLayer(globalServerConfig.GetCredential())
logger.FatalIf(err, "Unable to initialize gateway layer") logger.FatalIf(err, "Unable to initialize gateway layer")

View File

@ -20,10 +20,10 @@ import (
"context" "context"
"time" "time"
"github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/madmin"
"github.com/minio/minio/pkg/policy"
) )
// GatewayUnsupported list of unsupported call stubs for gateway. // GatewayUnsupported list of unsupported call stubs for gateway.
@ -72,15 +72,15 @@ func (a GatewayUnsupported) CompleteMultipartUpload(ctx context.Context, bucket
} }
// SetBucketPolicy sets policy on bucket // SetBucketPolicy sets policy on bucket
func (a GatewayUnsupported) SetBucketPolicy(ctx context.Context, bucket string, policyInfo policy.BucketAccessPolicy) error { func (a GatewayUnsupported) SetBucketPolicy(ctx context.Context, bucket string, bucketPolicy *policy.Policy) error {
logger.LogIf(ctx, NotImplemented{}) logger.LogIf(ctx, NotImplemented{})
return NotImplemented{} return NotImplemented{}
} }
// GetBucketPolicy will get policy on bucket // GetBucketPolicy will get policy on bucket
func (a GatewayUnsupported) GetBucketPolicy(ctx context.Context, bucket string) (bal policy.BucketAccessPolicy, err error) { func (a GatewayUnsupported) GetBucketPolicy(ctx context.Context, bucket string) (bucketPolicy *policy.Policy, err error) {
logger.LogIf(ctx, NotImplemented{}) logger.LogIf(ctx, NotImplemented{})
return bal, NotImplemented{} return nil, NotImplemented{}
} }
// DeleteBucketPolicy deletes all policies on bucket // DeleteBucketPolicy deletes all policies on bucket

View File

@ -34,10 +34,12 @@ import (
"github.com/Azure/azure-sdk-for-go/storage" "github.com/Azure/azure-sdk-for-go/storage"
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
"github.com/minio/cli" "github.com/minio/cli"
"github.com/minio/minio-go/pkg/policy" miniogopolicy "github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/policy/condition"
sha256 "github.com/minio/sha256-simd" sha256 "github.com/minio/sha256-simd"
minio "github.com/minio/minio/cmd" minio "github.com/minio/minio/cmd"
@ -1020,10 +1022,16 @@ func (a *azureObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
// storage.ContainerAccessTypePrivate - none in minio terminology // storage.ContainerAccessTypePrivate - none in minio terminology
// As the common denominator for minio and azure is readonly and none, we support // As the common denominator for minio and azure is readonly and none, we support
// these two policies at the bucket level. // these two policies at the bucket level.
func (a *azureObjects) SetBucketPolicy(ctx context.Context, bucket string, policyInfo policy.BucketAccessPolicy) error { func (a *azureObjects) SetBucketPolicy(ctx context.Context, bucket string, bucketPolicy *policy.Policy) error {
var policies []minio.BucketAccessPolicy policyInfo, err := minio.PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
// This should not happen.
logger.LogIf(ctx, err)
return azureToObjectError(err, bucket)
}
for prefix, policy := range policy.GetPolicies(policyInfo.Statements, bucket, "") { var policies []minio.BucketAccessPolicy
for prefix, policy := range miniogopolicy.GetPolicies(policyInfo.Statements, bucket, "") {
policies = append(policies, minio.BucketAccessPolicy{ policies = append(policies, minio.BucketAccessPolicy{
Prefix: prefix, Prefix: prefix,
Policy: policy, Policy: policy,
@ -1038,7 +1046,7 @@ func (a *azureObjects) SetBucketPolicy(ctx context.Context, bucket string, polic
logger.LogIf(ctx, minio.NotImplemented{}) logger.LogIf(ctx, minio.NotImplemented{})
return minio.NotImplemented{} return minio.NotImplemented{}
} }
if policies[0].Policy != policy.BucketPolicyReadOnly { if policies[0].Policy != miniogopolicy.BucketPolicyReadOnly {
logger.LogIf(ctx, minio.NotImplemented{}) logger.LogIf(ctx, minio.NotImplemented{})
return minio.NotImplemented{} return minio.NotImplemented{}
} }
@ -1047,31 +1055,47 @@ func (a *azureObjects) SetBucketPolicy(ctx context.Context, bucket string, polic
AccessPolicies: nil, AccessPolicies: nil,
} }
container := a.client.GetContainerReference(bucket) container := a.client.GetContainerReference(bucket)
err := container.SetPermissions(perm, nil) err = container.SetPermissions(perm, nil)
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return azureToObjectError(err, bucket) return azureToObjectError(err, bucket)
} }
// GetBucketPolicy - Get the container ACL and convert it to canonical []bucketAccessPolicy // GetBucketPolicy - Get the container ACL and convert it to canonical []bucketAccessPolicy
func (a *azureObjects) GetBucketPolicy(ctx context.Context, bucket string) (policy.BucketAccessPolicy, error) { func (a *azureObjects) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
policyInfo := policy.BucketAccessPolicy{Version: "2012-10-17"}
container := a.client.GetContainerReference(bucket) container := a.client.GetContainerReference(bucket)
perm, err := container.GetPermissions(nil) perm, err := container.GetPermissions(nil)
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return policy.BucketAccessPolicy{}, azureToObjectError(err, bucket) return nil, azureToObjectError(err, bucket)
} }
switch perm.AccessType {
case storage.ContainerAccessTypePrivate: if perm.AccessType == storage.ContainerAccessTypePrivate {
logger.LogIf(ctx, minio.PolicyNotFound{Bucket: bucket}) logger.LogIf(ctx, minio.BucketPolicyNotFound{Bucket: bucket})
return policy.BucketAccessPolicy{}, minio.PolicyNotFound{Bucket: bucket} return nil, minio.BucketPolicyNotFound{Bucket: bucket}
case storage.ContainerAccessTypeContainer: } else if perm.AccessType != storage.ContainerAccessTypeContainer {
policyInfo.Statements = policy.SetPolicy(policyInfo.Statements, policy.BucketPolicyReadOnly, bucket, "")
default:
logger.LogIf(ctx, minio.NotImplemented{}) logger.LogIf(ctx, minio.NotImplemented{})
return policy.BucketAccessPolicy{}, azureToObjectError(minio.NotImplemented{}) return nil, azureToObjectError(minio.NotImplemented{})
} }
return policyInfo, nil
return &policy.Policy{
Version: policy.DefaultVersion,
Statements: []policy.Statement{
policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(
policy.GetBucketLocationAction,
policy.ListBucketAction,
policy.GetObjectAction,
),
policy.NewResourceSet(
policy.NewResource(bucket, ""),
policy.NewResource(bucket, "*"),
),
condition.NewFunctions(),
),
},
}, nil
} }
// DeleteBucketPolicy - Set the container ACL to "private" // DeleteBucketPolicy - Set the container ACL to "private"

View File

@ -30,10 +30,12 @@ import (
b2 "github.com/minio/blazer/base" b2 "github.com/minio/blazer/base"
"github.com/minio/cli" "github.com/minio/cli"
"github.com/minio/minio-go/pkg/policy" miniogopolicy "github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
h2 "github.com/minio/minio/pkg/hash" h2 "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/policy/condition"
minio "github.com/minio/minio/cmd" minio "github.com/minio/minio/cmd"
) )
@ -722,10 +724,15 @@ func (l *b2Objects) CompleteMultipartUpload(ctx context.Context, bucket string,
// bucketType.AllPublic - bucketTypeReadOnly means that anybody can download the files is the bucket; // bucketType.AllPublic - bucketTypeReadOnly means that anybody can download the files is the bucket;
// bucketType.AllPrivate - bucketTypePrivate means that you need an authorization token to download them. // bucketType.AllPrivate - bucketTypePrivate means that you need an authorization token to download them.
// Default is AllPrivate for all buckets. // Default is AllPrivate for all buckets.
func (l *b2Objects) SetBucketPolicy(ctx context.Context, bucket string, policyInfo policy.BucketAccessPolicy) error { func (l *b2Objects) SetBucketPolicy(ctx context.Context, bucket string, bucketPolicy *policy.Policy) error {
var policies []minio.BucketAccessPolicy policyInfo, err := minio.PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
// This should not happen.
return b2ToObjectError(err, bucket)
}
for prefix, policy := range policy.GetPolicies(policyInfo.Statements, bucket, "") { var policies []minio.BucketAccessPolicy
for prefix, policy := range miniogopolicy.GetPolicies(policyInfo.Statements, bucket, "") {
policies = append(policies, minio.BucketAccessPolicy{ policies = append(policies, minio.BucketAccessPolicy{
Prefix: prefix, Prefix: prefix,
Policy: policy, Policy: policy,
@ -740,7 +747,7 @@ func (l *b2Objects) SetBucketPolicy(ctx context.Context, bucket string, policyIn
logger.LogIf(ctx, minio.NotImplemented{}) logger.LogIf(ctx, minio.NotImplemented{})
return minio.NotImplemented{} return minio.NotImplemented{}
} }
if policies[0].Policy != policy.BucketPolicyReadOnly { if policies[0].Policy != miniogopolicy.BucketPolicyReadOnly {
logger.LogIf(ctx, minio.NotImplemented{}) logger.LogIf(ctx, minio.NotImplemented{})
return minio.NotImplemented{} return minio.NotImplemented{}
} }
@ -756,21 +763,39 @@ func (l *b2Objects) SetBucketPolicy(ctx context.Context, bucket string, policyIn
// GetBucketPolicy, returns the current bucketType from B2 backend and convert // GetBucketPolicy, returns the current bucketType from B2 backend and convert
// it into S3 compatible bucket policy info. // it into S3 compatible bucket policy info.
func (l *b2Objects) GetBucketPolicy(ctx context.Context, bucket string) (policy.BucketAccessPolicy, error) { func (l *b2Objects) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
policyInfo := policy.BucketAccessPolicy{Version: "2012-10-17"}
bkt, err := l.Bucket(ctx, bucket) bkt, err := l.Bucket(ctx, bucket)
if err != nil { if err != nil {
return policyInfo, err return nil, err
}
if bkt.Type == bucketTypeReadOnly {
policyInfo.Statements = policy.SetPolicy(policyInfo.Statements, policy.BucketPolicyReadOnly, bucket, "")
return policyInfo, nil
} }
// bkt.Type can also be snapshot, but it is only allowed through B2 browser console, // bkt.Type can also be snapshot, but it is only allowed through B2 browser console,
// just return back as policy not found for all cases. // just return back as policy not found for all cases.
// CreateBucket always sets the value to allPrivate by default. // CreateBucket always sets the value to allPrivate by default.
logger.LogIf(ctx, minio.PolicyNotFound{Bucket: bucket}) if bkt.Type != bucketTypeReadOnly {
return policy.BucketAccessPolicy{}, minio.PolicyNotFound{Bucket: bucket} logger.LogIf(ctx, minio.BucketPolicyNotFound{Bucket: bucket})
return nil, minio.BucketPolicyNotFound{Bucket: bucket}
}
return &policy.Policy{
Version: policy.DefaultVersion,
Statements: []policy.Statement{
policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(
policy.GetBucketLocationAction,
policy.ListBucketAction,
policy.GetObjectAction,
),
policy.NewResourceSet(
policy.NewResource(bucket, ""),
policy.NewResource(bucket, "*"),
),
condition.NewFunctions(),
),
},
}, nil
} }
// DeleteBucketPolicy - resets the bucketType of bucket on B2 to 'allPrivate'. // DeleteBucketPolicy - resets the bucketType of bucket on B2 to 'allPrivate'.

View File

@ -33,10 +33,12 @@ import (
"cloud.google.com/go/storage" "cloud.google.com/go/storage"
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
"github.com/minio/cli" "github.com/minio/cli"
"github.com/minio/minio-go/pkg/policy" miniogopolicy "github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/policy/condition"
"google.golang.org/api/googleapi" "google.golang.org/api/googleapi"
"google.golang.org/api/iterator" "google.golang.org/api/iterator"
@ -1132,10 +1134,16 @@ func (l *gcsGateway) CompleteMultipartUpload(ctx context.Context, bucket string,
} }
// SetBucketPolicy - Set policy on bucket // SetBucketPolicy - Set policy on bucket
func (l *gcsGateway) SetBucketPolicy(ctx context.Context, bucket string, policyInfo policy.BucketAccessPolicy) error { func (l *gcsGateway) SetBucketPolicy(ctx context.Context, bucket string, bucketPolicy *policy.Policy) error {
var policies []minio.BucketAccessPolicy policyInfo, err := minio.PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
// This should not happen.
logger.LogIf(ctx, err)
return gcsToObjectError(err, bucket)
}
for prefix, policy := range policy.GetPolicies(policyInfo.Statements, bucket, "") { var policies []minio.BucketAccessPolicy
for prefix, policy := range miniogopolicy.GetPolicies(policyInfo.Statements, bucket, "") {
policies = append(policies, minio.BucketAccessPolicy{ policies = append(policies, minio.BucketAccessPolicy{
Prefix: prefix, Prefix: prefix,
Policy: policy, Policy: policy,
@ -1154,7 +1162,7 @@ func (l *gcsGateway) SetBucketPolicy(ctx context.Context, bucket string, policyI
} }
acl := l.client.Bucket(bucket).ACL() acl := l.client.Bucket(bucket).ACL()
if policies[0].Policy == policy.BucketPolicyNone { if policies[0].Policy == miniogopolicy.BucketPolicyNone {
if err := acl.Delete(l.ctx, storage.AllUsers); err != nil { if err := acl.Delete(l.ctx, storage.AllUsers); err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return gcsToObjectError(err, bucket) return gcsToObjectError(err, bucket)
@ -1164,9 +1172,9 @@ func (l *gcsGateway) SetBucketPolicy(ctx context.Context, bucket string, policyI
var role storage.ACLRole var role storage.ACLRole
switch policies[0].Policy { switch policies[0].Policy {
case policy.BucketPolicyReadOnly: case miniogopolicy.BucketPolicyReadOnly:
role = storage.RoleReader role = storage.RoleReader
case policy.BucketPolicyWriteOnly: case miniogopolicy.BucketPolicyWriteOnly:
role = storage.RoleWriter role = storage.RoleWriter
default: default:
logger.LogIf(ctx, minio.NotImplemented{}) logger.LogIf(ctx, minio.NotImplemented{})
@ -1182,30 +1190,63 @@ func (l *gcsGateway) SetBucketPolicy(ctx context.Context, bucket string, policyI
} }
// GetBucketPolicy - Get policy on bucket // GetBucketPolicy - Get policy on bucket
func (l *gcsGateway) GetBucketPolicy(ctx context.Context, bucket string) (policy.BucketAccessPolicy, error) { func (l *gcsGateway) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
rules, err := l.client.Bucket(bucket).ACL().List(l.ctx) rules, err := l.client.Bucket(bucket).ACL().List(l.ctx)
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return policy.BucketAccessPolicy{}, gcsToObjectError(err, bucket) return nil, gcsToObjectError(err, bucket)
} }
policyInfo := policy.BucketAccessPolicy{Version: "2012-10-17"}
var readOnly, writeOnly bool
for _, r := range rules { for _, r := range rules {
if r.Entity != storage.AllUsers || r.Role == storage.RoleOwner { if r.Entity != storage.AllUsers || r.Role == storage.RoleOwner {
continue continue
} }
switch r.Role { switch r.Role {
case storage.RoleReader: case storage.RoleReader:
policyInfo.Statements = policy.SetPolicy(policyInfo.Statements, policy.BucketPolicyReadOnly, bucket, "") readOnly = true
case storage.RoleWriter: case storage.RoleWriter:
policyInfo.Statements = policy.SetPolicy(policyInfo.Statements, policy.BucketPolicyWriteOnly, bucket, "") writeOnly = true
} }
} }
// Return NoSuchBucketPolicy error, when policy is not set
if len(policyInfo.Statements) == 0 { actionSet := policy.NewActionSet()
logger.LogIf(ctx, minio.PolicyNotFound{}) if readOnly {
return policy.BucketAccessPolicy{}, gcsToObjectError(minio.PolicyNotFound{}, bucket) actionSet.Add(policy.GetBucketLocationAction)
actionSet.Add(policy.ListBucketAction)
actionSet.Add(policy.GetObjectAction)
} }
return policyInfo, nil if writeOnly {
actionSet.Add(policy.GetBucketLocationAction)
actionSet.Add(policy.ListBucketMultipartUploadsAction)
actionSet.Add(policy.AbortMultipartUploadAction)
actionSet.Add(policy.DeleteObjectAction)
actionSet.Add(policy.ListMultipartUploadPartsAction)
actionSet.Add(policy.PutObjectAction)
}
// Return NoSuchBucketPolicy error, when policy is not set
if len(actionSet) == 0 {
logger.LogIf(ctx, minio.BucketPolicyNotFound{})
return nil, gcsToObjectError(minio.BucketPolicyNotFound{}, bucket)
}
return &policy.Policy{
Version: policy.DefaultVersion,
Statements: []policy.Statement{
policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
actionSet,
policy.NewResourceSet(
policy.NewResource(bucket, ""),
policy.NewResource(bucket, "*"),
),
condition.NewFunctions(),
),
},
}, nil
} }
// DeleteBucketPolicy - Delete all policies on bucket // DeleteBucketPolicy - Delete all policies on bucket

View File

@ -20,9 +20,9 @@ import (
"context" "context"
"github.com/minio/cli" "github.com/minio/cli"
"github.com/minio/minio-go/pkg/policy"
minio "github.com/minio/minio/cmd" minio "github.com/minio/minio/cmd"
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/policy"
) )
const ( const (
@ -132,6 +132,6 @@ func (l *nasObjects) IsNotificationSupported() bool {
} }
// GetBucketPolicy will get policy on bucket // GetBucketPolicy will get policy on bucket
func (l *nasObjects) GetBucketPolicy(ctx context.Context, bucket string) (policy.BucketAccessPolicy, error) { func (l *nasObjects) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
return minio.ReadBucketPolicy(bucket, l) return minio.GetPolicyConfig(l, bucket)
} }

View File

@ -30,11 +30,13 @@ import (
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/minio/cli" "github.com/minio/cli"
"github.com/minio/minio-go/pkg/policy" miniogopolicy "github.com/minio/minio-go/pkg/policy"
minio "github.com/minio/minio/cmd" minio "github.com/minio/minio/cmd"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/policy/condition"
) )
const ( const (
@ -962,8 +964,14 @@ func (l *ossObjects) CompleteMultipartUpload(ctx context.Context, bucket, object
// oss.ACLPublicReadWrite: readwrite in minio terminology // oss.ACLPublicReadWrite: readwrite in minio terminology
// oss.ACLPublicRead: readonly in minio terminology // oss.ACLPublicRead: readonly in minio terminology
// oss.ACLPrivate: none in minio terminology // oss.ACLPrivate: none in minio terminology
func (l *ossObjects) SetBucketPolicy(ctx context.Context, bucket string, policyInfo policy.BucketAccessPolicy) error { func (l *ossObjects) SetBucketPolicy(ctx context.Context, bucket string, bucketPolicy *policy.Policy) error {
bucketPolicies := policy.GetPolicies(policyInfo.Statements, bucket, "") policyInfo, err := minio.PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
// This should not happen.
return ossToObjectError(err, bucket)
}
bucketPolicies := miniogopolicy.GetPolicies(policyInfo.Statements, bucket, "")
if len(bucketPolicies) != 1 { if len(bucketPolicies) != 1 {
logger.LogIf(ctx, minio.NotImplemented{}) logger.LogIf(ctx, minio.NotImplemented{})
return minio.NotImplemented{} return minio.NotImplemented{}
@ -978,11 +986,11 @@ func (l *ossObjects) SetBucketPolicy(ctx context.Context, bucket string, policyI
var acl oss.ACLType var acl oss.ACLType
switch bucketPolicy { switch bucketPolicy {
case policy.BucketPolicyNone: case miniogopolicy.BucketPolicyNone:
acl = oss.ACLPrivate acl = oss.ACLPrivate
case policy.BucketPolicyReadOnly: case miniogopolicy.BucketPolicyReadOnly:
acl = oss.ACLPublicRead acl = oss.ACLPublicRead
case policy.BucketPolicyReadWrite: case miniogopolicy.BucketPolicyReadWrite:
acl = oss.ACLPublicReadWrite acl = oss.ACLPublicReadWrite
default: default:
logger.LogIf(ctx, minio.NotImplemented{}) logger.LogIf(ctx, minio.NotImplemented{})
@ -1000,29 +1008,60 @@ func (l *ossObjects) SetBucketPolicy(ctx context.Context, bucket string, policyI
} }
// GetBucketPolicy will get policy on bucket. // GetBucketPolicy will get policy on bucket.
func (l *ossObjects) GetBucketPolicy(ctx context.Context, bucket string) (policy.BucketAccessPolicy, error) { func (l *ossObjects) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
result, err := l.Client.GetBucketACL(bucket) result, err := l.Client.GetBucketACL(bucket)
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return policy.BucketAccessPolicy{}, ossToObjectError(err) return nil, ossToObjectError(err)
} }
policyInfo := policy.BucketAccessPolicy{Version: "2012-10-17"} var readOnly, readWrite bool
switch result.ACL { switch result.ACL {
case string(oss.ACLPrivate): case string(oss.ACLPrivate):
// By default, all buckets starts with a "private" policy. // By default, all buckets starts with a "private" policy.
logger.LogIf(ctx, minio.PolicyNotFound{}) logger.LogIf(ctx, minio.BucketPolicyNotFound{})
return policy.BucketAccessPolicy{}, ossToObjectError(minio.PolicyNotFound{}, bucket) return nil, ossToObjectError(minio.BucketPolicyNotFound{}, bucket)
case string(oss.ACLPublicRead): case string(oss.ACLPublicRead):
policyInfo.Statements = policy.SetPolicy(policyInfo.Statements, policy.BucketPolicyReadOnly, bucket, "") readOnly = true
case string(oss.ACLPublicReadWrite): case string(oss.ACLPublicReadWrite):
policyInfo.Statements = policy.SetPolicy(policyInfo.Statements, policy.BucketPolicyReadWrite, bucket, "") readWrite = true
default: default:
logger.LogIf(ctx, minio.NotImplemented{}) logger.LogIf(ctx, minio.NotImplemented{})
return policy.BucketAccessPolicy{}, minio.NotImplemented{} return nil, minio.NotImplemented{}
} }
return policyInfo, nil actionSet := policy.NewActionSet()
if readOnly {
actionSet.Add(policy.GetBucketLocationAction)
actionSet.Add(policy.ListBucketAction)
actionSet.Add(policy.GetObjectAction)
}
if readWrite {
actionSet.Add(policy.GetBucketLocationAction)
actionSet.Add(policy.ListBucketAction)
actionSet.Add(policy.GetObjectAction)
actionSet.Add(policy.ListBucketMultipartUploadsAction)
actionSet.Add(policy.AbortMultipartUploadAction)
actionSet.Add(policy.DeleteObjectAction)
actionSet.Add(policy.ListMultipartUploadPartsAction)
actionSet.Add(policy.PutObjectAction)
}
return &policy.Policy{
Version: policy.DefaultVersion,
Statements: []policy.Statement{
policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
actionSet,
policy.NewResourceSet(
policy.NewResource(bucket, ""),
policy.NewResource(bucket, "*"),
),
condition.NewFunctions(),
),
},
}, nil
} }
// DeleteBucketPolicy deletes all policies on bucket. // DeleteBucketPolicy deletes all policies on bucket.

View File

@ -20,14 +20,15 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"io" "io"
"strings"
"github.com/minio/cli" "github.com/minio/cli"
miniogo "github.com/minio/minio-go" miniogo "github.com/minio/minio-go"
"github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio-go/pkg/s3utils" "github.com/minio/minio-go/pkg/s3utils"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/policy"
minio "github.com/minio/minio/cmd" minio "github.com/minio/minio/cmd"
) )
@ -428,31 +429,32 @@ func (l *s3Objects) CompleteMultipartUpload(ctx context.Context, bucket string,
} }
// SetBucketPolicy sets policy on bucket // SetBucketPolicy sets policy on bucket
func (l *s3Objects) SetBucketPolicy(ctx context.Context, bucket string, policyInfo policy.BucketAccessPolicy) error { func (l *s3Objects) SetBucketPolicy(ctx context.Context, bucket string, bucketPolicy *policy.Policy) error {
data, err := json.Marshal(&policyInfo) data, err := json.Marshal(bucketPolicy)
if err != nil { if err != nil {
return err // This should not happen.
logger.LogIf(ctx, err)
return minio.ErrorRespToObjectError(err, bucket)
} }
if err := l.Client.SetBucketPolicy(bucket, string(data)); err != nil { if err := l.Client.SetBucketPolicy(bucket, string(data)); err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return minio.ErrorRespToObjectError(err, bucket, "") return minio.ErrorRespToObjectError(err, bucket)
} }
return nil return nil
} }
// GetBucketPolicy will get policy on bucket // GetBucketPolicy will get policy on bucket
func (l *s3Objects) GetBucketPolicy(ctx context.Context, bucket string) (policy.BucketAccessPolicy, error) { func (l *s3Objects) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
data, err := l.Client.GetBucketPolicy(bucket) data, err := l.Client.GetBucketPolicy(bucket)
if err != nil { if err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return policy.BucketAccessPolicy{}, minio.ErrorRespToObjectError(err, bucket, "") return nil, minio.ErrorRespToObjectError(err, bucket)
} }
var policyInfo policy.BucketAccessPolicy
if err = json.Unmarshal([]byte(data), &policyInfo); err != nil { bucketPolicy, err := policy.ParseConfig(strings.NewReader(data), bucket)
return policyInfo, err return bucketPolicy, minio.ErrorRespToObjectError(err, bucket)
}
return policyInfo, nil
} }
// DeleteBucketPolicy deletes all policies on bucket // DeleteBucketPolicy deletes all policies on bucket

View File

@ -52,7 +52,7 @@ func TestS3ToObjectError(t *testing.T) {
}, },
{ {
inputErr: errResponse("NoSuchBucketPolicy"), inputErr: errResponse("NoSuchBucketPolicy"),
expectedErr: minio.PolicyNotFound{}, expectedErr: minio.BucketPolicyNotFound{},
}, },
{ {
inputErr: errResponse("NoSuchBucket"), inputErr: errResponse("NoSuchBucket"),

View File

@ -120,6 +120,7 @@ var (
globalMinioHost = "" globalMinioHost = ""
globalNotificationSys *NotificationSys globalNotificationSys *NotificationSys
globalPolicySys *PolicySys
// CA root certificates, a nil value means system certs pool will be used // CA root certificates, a nil value means system certs pool will be used
globalRootCAs *x509.CertPool globalRootCAs *x509.CertPool

View File

@ -21,6 +21,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"path" "path"
@ -30,6 +31,7 @@ import (
"github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
xnet "github.com/minio/minio/pkg/net" xnet "github.com/minio/minio/pkg/net"
"github.com/minio/minio/pkg/policy"
) )
// NotificationSys - notification system. // NotificationSys - notification system.
@ -75,15 +77,33 @@ func (sys *NotificationSys) DeleteBucket(bucketName string) map[xnet.Host]error
return errors return errors
} }
// UpdateBucketPolicy - calls UpdateBucketPolicy RPC call on all peers. // SetBucketPolicy - calls SetBucketPolicy RPC call on all peers.
func (sys *NotificationSys) UpdateBucketPolicy(bucketName string) map[xnet.Host]error { func (sys *NotificationSys) SetBucketPolicy(bucketName string, bucketPolicy *policy.Policy) map[xnet.Host]error {
errors := make(map[xnet.Host]error) errors := make(map[xnet.Host]error)
var wg sync.WaitGroup var wg sync.WaitGroup
for addr, client := range sys.peerRPCClientMap { for addr, client := range sys.peerRPCClientMap {
wg.Add(1) wg.Add(1)
go func(addr xnet.Host, client *PeerRPCClient) { go func(addr xnet.Host, client *PeerRPCClient) {
defer wg.Done() defer wg.Done()
if err := client.UpdateBucketPolicy(bucketName); err != nil { if err := client.SetBucketPolicy(bucketName, bucketPolicy); err != nil {
errors[addr] = err
}
}(addr, client)
}
wg.Wait()
return errors
}
// RemoveBucketPolicy - calls RemoveBucketPolicy RPC call on all peers.
func (sys *NotificationSys) RemoveBucketPolicy(bucketName string) map[xnet.Host]error {
errors := make(map[xnet.Host]error)
var wg sync.WaitGroup
for addr, client := range sys.peerRPCClientMap {
wg.Add(1)
go func(addr xnet.Host, client *PeerRPCClient) {
defer wg.Done()
if err := client.RemoveBucketPolicy(bucketName); err != nil {
errors[addr] = err errors[addr] = err
} }
}(addr, client) }(addr, client)
@ -182,7 +202,7 @@ func (sys *NotificationSys) initListeners(ctx context.Context, objAPI ObjectLaye
defer objLock.Unlock() defer objLock.Unlock()
reader, e := readConfig(ctx, objAPI, configFile) reader, e := readConfig(ctx, objAPI, configFile)
if e != nil && !IsErrIgnored(e, errDiskNotFound, errNoSuchNotifications) { if e != nil && !IsErrIgnored(e, errDiskNotFound, errConfigNotFound) {
return e return e
} }
@ -433,7 +453,7 @@ func (args eventArgs) ToEvent() event.Event {
Bucket: event.Bucket{ Bucket: event.Bucket{
Name: args.BucketName, Name: args.BucketName,
OwnerIdentity: event.Identity{creds.AccessKey}, OwnerIdentity: event.Identity{creds.AccessKey},
ARN: bucketARNPrefix + args.BucketName, ARN: policy.ResourceARNPrefix + args.BucketName,
}, },
Object: event.Object{ Object: event.Object{
Key: url.QueryEscape(args.Object.Name), Key: url.QueryEscape(args.Object.Name),
@ -483,6 +503,8 @@ func saveConfig(objAPI ObjectLayer, configFile string, data []byte) error {
return err return err
} }
var errConfigNotFound = errors.New("config file not found")
func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) (*bytes.Buffer, error) { func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) (*bytes.Buffer, error) {
var buffer bytes.Buffer var buffer bytes.Buffer
// Read entire content by setting size to -1 // Read entire content by setting size to -1
@ -490,8 +512,9 @@ func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) (*by
if err != nil { if err != nil {
// Ignore if err is ObjectNotFound or IncompleteBody when bucket is not configured with notification // Ignore if err is ObjectNotFound or IncompleteBody when bucket is not configured with notification
if isErrObjectNotFound(err) || isErrIncompleteBody(err) { if isErrObjectNotFound(err) || isErrIncompleteBody(err) {
return nil, errNoSuchNotifications return nil, errConfigNotFound
} }
logger.GetReqInfo(ctx).AppendTags("configFile", configFile) logger.GetReqInfo(ctx).AppendTags("configFile", configFile)
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
return nil, err return nil, err
@ -510,6 +533,10 @@ func readNotificationConfig(ctx context.Context, objAPI ObjectLayer, bucketName
configFile := path.Join(bucketConfigPrefix, bucketName, bucketNotificationConfig) configFile := path.Join(bucketConfigPrefix, bucketName, bucketNotificationConfig)
reader, err := readConfig(ctx, objAPI, configFile) reader, err := readConfig(ctx, objAPI, configFile)
if err != nil { if err != nil {
if err == errConfigNotFound {
err = errNoSuchNotifications
}
return nil, err return nil, err
} }
@ -551,7 +578,7 @@ func SaveListener(objAPI ObjectLayer, bucketName string, eventNames []event.Name
defer objLock.Unlock() defer objLock.Unlock()
reader, err := readConfig(ctx, objAPI, configFile) reader, err := readConfig(ctx, objAPI, configFile)
if err != nil && !IsErrIgnored(err, errDiskNotFound, errNoSuchNotifications) { if err != nil && !IsErrIgnored(err, errDiskNotFound, errConfigNotFound) {
return err return err
} }
@ -602,7 +629,7 @@ func RemoveListener(objAPI ObjectLayer, bucketName string, targetID event.Target
defer objLock.Unlock() defer objLock.Unlock()
reader, err := readConfig(ctx, objAPI, configFile) reader, err := readConfig(ctx, objAPI, configFile)
if err != nil && !IsErrIgnored(err, errDiskNotFound, errNoSuchNotifications) { if err != nil && !IsErrIgnored(err, errDiskNotFound, errConfigNotFound) {
return err return err
} }

View File

@ -85,7 +85,7 @@ func dirObjectInfo(bucket, object string, size int64, metadata map[string]string
func deleteBucketMetadata(ctx context.Context, bucket string, objAPI ObjectLayer) { func deleteBucketMetadata(ctx context.Context, bucket string, objAPI ObjectLayer) {
// Delete bucket access policy, if present - ignore any errors. // Delete bucket access policy, if present - ignore any errors.
removeBucketPolicy(ctx, bucket, objAPI) removePolicyConfig(ctx, objAPI, bucket)
// Delete notification config, if present - ignore any errors. // Delete notification config, if present - ignore any errors.
removeNotificationConfig(ctx, objAPI, bucket) removeNotificationConfig(ctx, objAPI, bucket)

View File

@ -366,13 +366,6 @@ func (e PolicyNesting) Error() string {
return "New bucket policy conflicts with an existing policy. Please try again with new prefix." return "New bucket policy conflicts with an existing policy. Please try again with new prefix."
} }
// PolicyNotFound - policy not found
type PolicyNotFound GenericError
func (e PolicyNotFound) Error() string {
return "Policy not found"
}
// UnsupportedMetadata - unsupported metadata // UnsupportedMetadata - unsupported metadata
type UnsupportedMetadata struct{} type UnsupportedMetadata struct{}
@ -396,15 +389,6 @@ func isErrIncompleteBody(err error) bool {
return false return false
} }
// isErrBucketPolicyNotFound - Check if error type is BucketPolicyNotFound.
func isErrBucketPolicyNotFound(err error) bool {
switch err.(type) {
case PolicyNotFound:
return true
}
return false
}
// isErrObjectNotFound - Check if error type is ObjectNotFound. // isErrObjectNotFound - Check if error type is ObjectNotFound.
func isErrObjectNotFound(err error) bool { func isErrObjectNotFound(err error) bool {
switch err.(type) { switch err.(type) {

View File

@ -21,9 +21,9 @@ import (
"io" "io"
"time" "time"
"github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/madmin"
"github.com/minio/minio/pkg/policy"
) )
// ObjectLayer implements primitives for object API layer. // ObjectLayer implements primitives for object API layer.
@ -70,9 +70,8 @@ type ObjectLayer interface {
ClearLocks(context.Context, []VolumeLockInfo) error ClearLocks(context.Context, []VolumeLockInfo) error
// Policy operations // Policy operations
SetBucketPolicy(context.Context, string, policy.BucketAccessPolicy) error SetBucketPolicy(context.Context, string, *policy.Policy) error
GetBucketPolicy(context.Context, string) (policy.BucketAccessPolicy, error) GetBucketPolicy(context.Context, string) (*policy.Policy, error)
RefreshBucketPolicy(context.Context, string) error
DeleteBucketPolicy(context.Context, string) error DeleteBucketPolicy(context.Context, string) error
// Supported operations check // Supported operations check

View File

@ -34,9 +34,9 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/handlers"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/ioutil" "github.com/minio/minio/pkg/ioutil"
"github.com/minio/minio/pkg/policy"
sha256 "github.com/minio/sha256-simd" sha256 "github.com/minio/sha256-simd"
"github.com/minio/sio" "github.com/minio/sio"
) )
@ -60,23 +60,6 @@ func setHeadGetRespHeaders(w http.ResponseWriter, reqParams url.Values) {
} }
} }
// errAllowableNotFound - For an anon user, return 404 if have ListBucket, 403 otherwise
// this is in keeping with the permissions sections of the docs of both:
// HEAD Object: http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html
// GET Object: http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGET.html
func errAllowableObjectNotFound(ctx context.Context, bucket string, r *http.Request) APIErrorCode {
if getRequestAuthType(r) == authTypeAnonymous {
// We care about the bucket as a whole, not a particular resource.
resource := "/" + bucket
sourceIP := handlers.GetSourceIP(r)
if s3Error := enforceBucketPolicy(ctx, bucket, "s3:ListBucket", resource,
r.Referer(), sourceIP, r.URL.Query()); s3Error != ErrNone {
return ErrAccessDenied
}
}
return ErrNoSuchKey
}
// GetObjectHandler - GET Object // GetObjectHandler - GET Object
// ---------- // ----------
// This implementation of the GET operation retrieves object. To use GET, // This implementation of the GET operation retrieves object. To use GET,
@ -96,7 +79,7 @@ func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Req
return return
} }
if s3Error := checkRequestAuthType(ctx, r, bucket, "s3:GetObject", globalServerConfig.GetRegion()); s3Error != ErrNone { if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectAction, bucket, object); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
@ -109,9 +92,21 @@ func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Req
objInfo, err := getObjectInfo(ctx, bucket, object) objInfo, err := getObjectInfo(ctx, bucket, object)
if err != nil { if err != nil {
apiErr := toAPIErrorCode(err) apiErr := toAPIErrorCode(err)
if apiErr == ErrNoSuchKey { if apiErr == ErrNoSuchKey && getRequestAuthType(r) == authTypeAnonymous {
apiErr = errAllowableObjectNotFound(ctx, bucket, r) // As per "Permission" section in https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGET.html
// If the object you request does not exist, the error Amazon S3 returns depends on whether you also have the s3:ListBucket permission.
// * If you have the s3:ListBucket permission on the bucket, Amazon S3 will return an HTTP status code 404 ("no such key") error.
// * if you dont have the s3:ListBucket permission, Amazon S3 will return an HTTP status code 403 ("access denied") error.`
if !globalPolicySys.IsAllowed(policy.Args{
Action: policy.ListBucketAction,
BucketName: bucket,
ConditionValues: getConditionValues(r, ""),
IsOwner: false,
}) {
apiErr = ErrAccessDenied
}
} }
writeErrorResponse(w, apiErr, r.URL) writeErrorResponse(w, apiErr, r.URL)
return return
} }
@ -232,7 +227,7 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
return return
} }
if s3Error := checkRequestAuthType(ctx, r, bucket, "s3:GetObject", globalServerConfig.GetRegion()); s3Error != ErrNone { if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectAction, bucket, object); s3Error != ErrNone {
writeErrorResponseHeadersOnly(w, s3Error) writeErrorResponseHeadersOnly(w, s3Error)
return return
} }
@ -245,9 +240,21 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
objInfo, err := getObjectInfo(ctx, bucket, object) objInfo, err := getObjectInfo(ctx, bucket, object)
if err != nil { if err != nil {
apiErr := toAPIErrorCode(err) apiErr := toAPIErrorCode(err)
if apiErr == ErrNoSuchKey { if apiErr == ErrNoSuchKey && getRequestAuthType(r) == authTypeAnonymous {
apiErr = errAllowableObjectNotFound(ctx, bucket, r) // As per "Permission" section in https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html
// If the object you request does not exist, the error Amazon S3 returns depends on whether you also have the s3:ListBucket permission.
// * If you have the s3:ListBucket permission on the bucket, Amazon S3 will return an HTTP status code 404 ("no such key") error.
// * if you dont have the s3:ListBucket permission, Amazon S3 will return an HTTP status code 403 ("access denied") error.`
if !globalPolicySys.IsAllowed(policy.Args{
Action: policy.ListBucketAction,
BucketName: bucket,
ConditionValues: getConditionValues(r, ""),
IsOwner: false,
}) {
apiErr = ErrAccessDenied
}
} }
writeErrorResponseHeadersOnly(w, apiErr) writeErrorResponseHeadersOnly(w, apiErr)
return return
} }
@ -340,7 +347,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
return return
} }
if s3Error := checkRequestAuthType(ctx, r, dstBucket, "s3:PutObject", globalServerConfig.GetRegion()); s3Error != ErrNone { if s3Error := checkRequestAuthType(ctx, r, policy.PutObjectAction, dstBucket, dstObject); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
@ -666,10 +673,14 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
writeErrorResponse(w, ErrAccessDenied, r.URL) writeErrorResponse(w, ErrAccessDenied, r.URL)
return return
case authTypeAnonymous: case authTypeAnonymous:
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html if !globalPolicySys.IsAllowed(policy.Args{
sourceIP := handlers.GetSourceIP(r) Action: policy.PutObjectAction,
if s3Err = enforceBucketPolicy(ctx, bucket, "s3:PutObject", r.URL.Path, r.Referer(), sourceIP, r.URL.Query()); s3Err != ErrNone { BucketName: bucket,
writeErrorResponse(w, s3Err, r.URL) ConditionValues: getConditionValues(r, ""),
IsOwner: false,
ObjectName: object,
}) {
writeErrorResponse(w, ErrAccessDenied, r.URL)
return return
} }
case authTypeStreamingSigned: case authTypeStreamingSigned:
@ -781,7 +792,7 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
return return
} }
if s3Error := checkRequestAuthType(ctx, r, bucket, "s3:PutObject", globalServerConfig.GetRegion()); s3Error != ErrNone { if s3Error := checkRequestAuthType(ctx, r, policy.PutObjectAction, bucket, object); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
@ -867,7 +878,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
return return
} }
if s3Error := checkRequestAuthType(ctx, r, dstBucket, "s3:PutObject", globalServerConfig.GetRegion()); s3Error != ErrNone { if s3Error := checkRequestAuthType(ctx, r, policy.PutObjectAction, dstBucket, dstObject); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
@ -1133,10 +1144,14 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
writeErrorResponse(w, ErrAccessDenied, r.URL) writeErrorResponse(w, ErrAccessDenied, r.URL)
return return
case authTypeAnonymous: case authTypeAnonymous:
// http://docs.aws.amazon.com/AmazonS3/latest/dev/mpuAndPermissions.html if !globalPolicySys.IsAllowed(policy.Args{
if s3Error := enforceBucketPolicy(ctx, bucket, "s3:PutObject", r.URL.Path, Action: policy.PutObjectAction,
r.Referer(), handlers.GetSourceIP(r), r.URL.Query()); s3Error != ErrNone { BucketName: bucket,
writeErrorResponse(w, s3Error, r.URL) ConditionValues: getConditionValues(r, ""),
IsOwner: false,
ObjectName: object,
}) {
writeErrorResponse(w, ErrAccessDenied, r.URL)
return return
} }
case authTypeStreamingSigned: case authTypeStreamingSigned:
@ -1262,7 +1277,8 @@ func (api objectAPIHandlers) AbortMultipartUploadHandler(w http.ResponseWriter,
if api.CacheAPI() != nil { if api.CacheAPI() != nil {
abortMultipartUpload = api.CacheAPI().AbortMultipartUpload abortMultipartUpload = api.CacheAPI().AbortMultipartUpload
} }
if s3Error := checkRequestAuthType(ctx, r, bucket, "s3:AbortMultipartUpload", globalServerConfig.GetRegion()); s3Error != ErrNone {
if s3Error := checkRequestAuthType(ctx, r, policy.AbortMultipartUploadAction, bucket, object); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
@ -1297,7 +1313,7 @@ func (api objectAPIHandlers) ListObjectPartsHandler(w http.ResponseWriter, r *ht
return return
} }
if s3Error := checkRequestAuthType(ctx, r, bucket, "s3:ListMultipartUploadParts", globalServerConfig.GetRegion()); s3Error != ErrNone { if s3Error := checkRequestAuthType(ctx, r, policy.ListMultipartUploadPartsAction, bucket, object); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
@ -1337,7 +1353,7 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
return return
} }
if s3Error := checkRequestAuthType(ctx, r, bucket, "s3:PutObject", globalServerConfig.GetRegion()); s3Error != ErrNone { if s3Error := checkRequestAuthType(ctx, r, policy.PutObjectAction, bucket, object); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }
@ -1446,7 +1462,7 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
return return
} }
if s3Error := checkRequestAuthType(ctx, r, bucket, "s3:DeleteObject", globalServerConfig.GetRegion()); s3Error != ErrNone { if s3Error := checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, object); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL) writeErrorResponse(w, s3Error, r.URL)
return return
} }

View File

@ -172,7 +172,7 @@ func testAPIHeadObjectHandler(obj ObjectLayer, instanceType, bucketName string,
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse, // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again. // unsigned request goes through and its validated again.
ExecObjectLayerAPIAnonTest(t, obj, "TestAPIHeadObjectHandler", bucketName, objectName, instanceType, apiRouter, anonReq, getReadOnlyObjectStatement) ExecObjectLayerAPIAnonTest(t, obj, "TestAPIHeadObjectHandler", bucketName, objectName, instanceType, apiRouter, anonReq, getAnonReadOnlyObjectPolicy(bucketName, objectName))
// HTTP request for testing when `objectLayer` is set to `nil`. // HTTP request for testing when `objectLayer` is set to `nil`.
// There is no need to use an existing bucket and valid input for creating the request // There is no need to use an existing bucket and valid input for creating the request
@ -445,7 +445,7 @@ func testAPIGetObjectHandler(obj ObjectLayer, instanceType, bucketName string, a
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse, // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again. // unsigned request goes through and its validated again.
ExecObjectLayerAPIAnonTest(t, obj, "TestAPIGetObjectHandler", bucketName, objectName, instanceType, apiRouter, anonReq, getReadOnlyObjectStatement) ExecObjectLayerAPIAnonTest(t, obj, "TestAPIGetObjectHandler", bucketName, objectName, instanceType, apiRouter, anonReq, getAnonReadOnlyObjectPolicy(bucketName, objectName))
// HTTP request for testing when `objectLayer` is set to `nil`. // HTTP request for testing when `objectLayer` is set to `nil`.
// There is no need to use an existing bucket and valid input for creating the request // There is no need to use an existing bucket and valid input for creating the request
@ -1001,7 +1001,7 @@ func testAPIPutObjectHandler(obj ObjectLayer, instanceType, bucketName string, a
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse, // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again. // unsigned request goes through and its validated again.
ExecObjectLayerAPIAnonTest(t, obj, "TestAPIPutObjectHandler", bucketName, objectName, instanceType, apiRouter, anonReq, getWriteOnlyObjectStatement) ExecObjectLayerAPIAnonTest(t, obj, "TestAPIPutObjectHandler", bucketName, objectName, instanceType, apiRouter, anonReq, getAnonWriteOnlyObjectPolicy(bucketName, objectName))
// HTTP request to test the case of `objectLayer` being set to `nil`. // HTTP request to test the case of `objectLayer` being set to `nil`.
// There is no need to use an existing bucket or valid input for creating the request, // There is no need to use an existing bucket or valid input for creating the request,
@ -1847,7 +1847,7 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse, // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again. // unsigned request goes through and its validated again.
ExecObjectLayerAPIAnonTest(t, obj, "TestAPICopyObjectHandler", bucketName, newCopyAnonObject, instanceType, apiRouter, anonReq, getWriteOnlyObjectStatement) ExecObjectLayerAPIAnonTest(t, obj, "TestAPICopyObjectHandler", bucketName, newCopyAnonObject, instanceType, apiRouter, anonReq, getAnonWriteOnlyObjectPolicy(bucketName, newCopyAnonObject))
// HTTP request to test the case of `objectLayer` being set to `nil`. // HTTP request to test the case of `objectLayer` being set to `nil`.
// There is no need to use an existing bucket or valid input for creating the request, // There is no need to use an existing bucket or valid input for creating the request,
@ -1998,7 +1998,7 @@ func testAPINewMultipartHandler(obj ObjectLayer, instanceType, bucketName string
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse, // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again. // unsigned request goes through and its validated again.
ExecObjectLayerAPIAnonTest(t, obj, "TestAPINewMultipartHandler", bucketName, objectName, instanceType, apiRouter, anonReq, getWriteOnlyObjectStatement) ExecObjectLayerAPIAnonTest(t, obj, "TestAPINewMultipartHandler", bucketName, objectName, instanceType, apiRouter, anonReq, getAnonWriteOnlyObjectPolicy(bucketName, objectName))
// HTTP request to test the case of `objectLayer` being set to `nil`. // HTTP request to test the case of `objectLayer` being set to `nil`.
// There is no need to use an existing bucket or valid input for creating the request, // There is no need to use an existing bucket or valid input for creating the request,
@ -2409,7 +2409,7 @@ func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName s
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again. // unsigned request goes through and its validated again.
ExecObjectLayerAPIAnonTest(t, obj, "TestAPICompleteMultipartHandler", bucketName, objectName, instanceType, ExecObjectLayerAPIAnonTest(t, obj, "TestAPICompleteMultipartHandler", bucketName, objectName, instanceType,
apiRouter, anonReq, getWriteOnlyObjectStatement) apiRouter, anonReq, getAnonWriteOnlyObjectPolicy(bucketName, objectName))
// HTTP request to test the case of `objectLayer` being set to `nil`. // HTTP request to test the case of `objectLayer` being set to `nil`.
// There is no need to use an existing bucket or valid input for creating the request, // There is no need to use an existing bucket or valid input for creating the request,
@ -2572,7 +2572,7 @@ func testAPIAbortMultipartHandler(obj ObjectLayer, instanceType, bucketName stri
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again. // unsigned request goes through and its validated again.
ExecObjectLayerAPIAnonTest(t, obj, "TestAPIAbortMultipartHandler", bucketName, objectName, instanceType, ExecObjectLayerAPIAnonTest(t, obj, "TestAPIAbortMultipartHandler", bucketName, objectName, instanceType,
apiRouter, anonReq, getWriteOnlyObjectStatement) apiRouter, anonReq, getAnonWriteOnlyObjectPolicy(bucketName, objectName))
// HTTP request to test the case of `objectLayer` being set to `nil`. // HTTP request to test the case of `objectLayer` being set to `nil`.
// There is no need to use an existing bucket or valid input for creating the request, // There is no need to use an existing bucket or valid input for creating the request,
@ -2734,7 +2734,7 @@ func testAPIDeleteObjectHandler(obj ObjectLayer, instanceType, bucketName string
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse, // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again. // unsigned request goes through and its validated again.
ExecObjectLayerAPIAnonTest(t, obj, "TestAPIDeleteObjectHandler", bucketName, anonObjectName, instanceType, apiRouter, anonReq, getWriteOnlyObjectStatement) ExecObjectLayerAPIAnonTest(t, obj, "TestAPIDeleteObjectHandler", bucketName, anonObjectName, instanceType, apiRouter, anonReq, getAnonWriteOnlyObjectPolicy(bucketName, anonObjectName))
// HTTP request to test the case of `objectLayer` being set to `nil`. // HTTP request to test the case of `objectLayer` being set to `nil`.
// There is no need to use an existing bucket or valid input for creating the request, // There is no need to use an existing bucket or valid input for creating the request,
@ -3205,7 +3205,7 @@ func testAPIPutObjectPartHandler(obj ObjectLayer, instanceType, bucketName strin
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse, // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again. // unsigned request goes through and its validated again.
ExecObjectLayerAPIAnonTest(t, obj, "TestAPIPutObjectPartHandler", bucketName, testObject, instanceType, apiRouter, anonReq, getWriteOnlyObjectStatement) ExecObjectLayerAPIAnonTest(t, obj, "TestAPIPutObjectPartHandler", bucketName, testObject, instanceType, apiRouter, anonReq, getAnonWriteOnlyObjectPolicy(bucketName, testObject))
// HTTP request for testing when `ObjectLayer` is set to `nil`. // HTTP request for testing when `ObjectLayer` is set to `nil`.
// There is no need to use an existing bucket and valid input for creating the request // There is no need to use an existing bucket and valid input for creating the request
@ -3508,7 +3508,7 @@ func testAPIListObjectPartsHandler(obj ObjectLayer, instanceType, bucketName str
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse, // ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the // sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
// unsigned request goes through and its validated again. // unsigned request goes through and its validated again.
ExecObjectLayerAPIAnonTest(t, obj, "TestAPIListObjectPartsHandler", bucketName, testObject, instanceType, apiRouter, anonReq, getWriteOnlyObjectStatement) ExecObjectLayerAPIAnonTest(t, obj, "TestAPIListObjectPartsHandler", bucketName, testObject, instanceType, apiRouter, anonReq, getAnonWriteOnlyObjectPolicy(bucketName, testObject))
// HTTP request for testing when `objectLayer` is set to `nil`. // HTTP request for testing when `objectLayer` is set to `nil`.
// There is no need to use an existing bucket and valid input for creating the request // There is no need to use an existing bucket and valid input for creating the request

View File

@ -25,6 +25,7 @@ import (
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event"
xnet "github.com/minio/minio/pkg/net" xnet "github.com/minio/minio/pkg/net"
"github.com/minio/minio/pkg/policy"
) )
const s3Path = "/s3/remote" const s3Path = "/s3/remote"
@ -43,23 +44,33 @@ type DeleteBucketArgs struct {
// DeleteBucket - handles delete bucket RPC call which removes all values of given bucket in global NotificationSys object. // DeleteBucket - handles delete bucket RPC call which removes all values of given bucket in global NotificationSys object.
func (receiver *PeerRPCReceiver) DeleteBucket(args *DeleteBucketArgs, reply *AuthRPCArgs) error { func (receiver *PeerRPCReceiver) DeleteBucket(args *DeleteBucketArgs, reply *AuthRPCArgs) error {
globalNotificationSys.RemoveNotification(args.BucketName) globalNotificationSys.RemoveNotification(args.BucketName)
globalPolicySys.Remove(args.BucketName)
return nil return nil
} }
// UpdateBucketPolicyArgs - update bucket policy RPC arguments. // SetBucketPolicyArgs - set bucket policy RPC arguments.
type UpdateBucketPolicyArgs struct { type SetBucketPolicyArgs struct {
AuthRPCArgs
BucketName string
Policy policy.Policy
}
// SetBucketPolicy - handles set bucket policy RPC call which adds bucket policy to globalPolicySys.
func (receiver *PeerRPCReceiver) SetBucketPolicy(args *SetBucketPolicyArgs, reply *AuthRPCArgs) error {
globalPolicySys.Set(args.BucketName, args.Policy)
return nil
}
// RemoveBucketPolicyArgs - delete bucket policy RPC arguments.
type RemoveBucketPolicyArgs struct {
AuthRPCArgs AuthRPCArgs
BucketName string BucketName string
} }
// UpdateBucketPolicy - handles update bucket policy RPC call which sets bucket policies to given bucket in global BucketPolicies object. // RemoveBucketPolicy - handles delete bucket policy RPC call which removes bucket policy to globalPolicySys.
func (receiver *PeerRPCReceiver) UpdateBucketPolicy(args *UpdateBucketPolicyArgs, reply *AuthRPCArgs) error { func (receiver *PeerRPCReceiver) RemoveBucketPolicy(args *RemoveBucketPolicyArgs, reply *AuthRPCArgs) error {
objectAPI := newObjectLayerFn() globalPolicySys.Remove(args.BucketName)
if objectAPI == nil { return nil
// If the object layer is just coming up then it will load the policy from the disk.
return nil
}
return objectAPI.RefreshBucketPolicy(context.Background(), args.BucketName)
} }
// PutBucketNotificationArgs - put bucket notification RPC arguments. // PutBucketNotificationArgs - put bucket notification RPC arguments.
@ -195,13 +206,23 @@ func (rpcClient *PeerRPCClient) DeleteBucket(bucketName string) error {
return rpcClient.Call("Peer.DeleteBucket", &args, &reply) return rpcClient.Call("Peer.DeleteBucket", &args, &reply)
} }
// UpdateBucketPolicy - calls update bucket policy RPC. // SetBucketPolicy - calls set bucket policy RPC.
func (rpcClient *PeerRPCClient) UpdateBucketPolicy(bucketName string) error { func (rpcClient *PeerRPCClient) SetBucketPolicy(bucketName string, bucketPolicy *policy.Policy) error {
args := UpdateBucketPolicyArgs{ args := SetBucketPolicyArgs{
BucketName: bucketName,
Policy: *bucketPolicy,
}
reply := AuthRPCReply{}
return rpcClient.Call("Peer.SetBucketPolicy", &args, &reply)
}
// RemoveBucketPolicy - calls remove bucket policy RPC.
func (rpcClient *PeerRPCClient) RemoveBucketPolicy(bucketName string) error {
args := RemoveBucketPolicyArgs{
BucketName: bucketName, BucketName: bucketName,
} }
reply := AuthRPCReply{} reply := AuthRPCReply{}
return rpcClient.Call("Peer.UpdateBucketPolicy", &args, &reply) return rpcClient.Call("Peer.RemoveBucketPolicy", &args, &reply)
} }
// PutBucketNotification - calls put bukcet notification RPC. // PutBucketNotification - calls put bukcet notification RPC.

218
cmd/policy.go Normal file
View File

@ -0,0 +1,218 @@
/*
* 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 cmd
import (
"context"
"encoding/json"
"net/http"
"path"
"sync"
miniogopolicy "github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio/pkg/handlers"
"github.com/minio/minio/pkg/policy"
)
// PolicySys - policy system.
type PolicySys struct {
sync.RWMutex
bucketPolicyMap map[string]policy.Policy
}
// Set - sets policy to given bucket name. If policy is empty, existing policy is removed.
func (sys *PolicySys) Set(bucketName string, policy policy.Policy) {
sys.Lock()
defer sys.Unlock()
if policy.IsEmpty() {
delete(sys.bucketPolicyMap, bucketName)
} else {
sys.bucketPolicyMap[bucketName] = policy
}
}
// Remove - removes policy for given bucket name.
func (sys *PolicySys) Remove(bucketName string) {
sys.Lock()
defer sys.Unlock()
delete(sys.bucketPolicyMap, bucketName)
}
// IsAllowed - checks given policy args is allowed to continue the Rest API.
func (sys *PolicySys) IsAllowed(args policy.Args) bool {
sys.RLock()
defer sys.RUnlock()
// If policy is available for given bucket, check the policy.
if p, found := sys.bucketPolicyMap[args.BucketName]; found {
return p.IsAllowed(args)
}
// As policy is not available for given bucket name, returns IsOwner i.e.
// operation is allowed only for owner.
return args.IsOwner
}
// Init - initializes policy system from policy.json of all buckets.
func (sys *PolicySys) Init(objAPI ObjectLayer) error {
if objAPI == nil {
return errInvalidArgument
}
buckets, err := objAPI.ListBuckets(context.Background())
if err != nil {
return err
}
for _, bucket := range buckets {
config, err := GetPolicyConfig(objAPI, bucket.Name)
if err != nil {
if _, ok := err.(BucketPolicyNotFound); !ok {
return err
}
} else {
sys.Set(bucket.Name, *config)
}
}
return nil
}
// NewPolicySys - creates new policy system.
func NewPolicySys() *PolicySys {
return &PolicySys{
bucketPolicyMap: make(map[string]policy.Policy),
}
}
func getConditionValues(request *http.Request, locationConstraint string) map[string][]string {
args := make(map[string][]string)
for key, values := range request.Header {
if existingValues, found := args[key]; found {
args[key] = append(existingValues, values...)
} else {
args[key] = values
}
}
for key, values := range request.URL.Query() {
if existingValues, found := args[key]; found {
args[key] = append(existingValues, values...)
} else {
args[key] = values
}
}
args["SourceIp"] = []string{handlers.GetSourceIP(request)}
if locationConstraint != "" {
args["LocationConstraint"] = []string{locationConstraint}
}
return args
}
// GetPolicyConfig - get policy config for given bucket name.
func GetPolicyConfig(objAPI ObjectLayer, bucketName string) (*policy.Policy, error) {
// Construct path to policy.json for the given bucket.
configFile := path.Join(bucketConfigPrefix, bucketName, bucketPolicyConfig)
reader, err := readConfig(context.Background(), objAPI, configFile)
if err != nil {
if err == errConfigNotFound {
err = BucketPolicyNotFound{Bucket: bucketName}
}
return nil, err
}
bucketPolicy, err := policy.ParseConfig(reader, bucketName)
if err != nil {
return nil, err
}
return bucketPolicy, nil
}
func savePolicyConfig(objAPI ObjectLayer, bucketName string, bucketPolicy *policy.Policy) error {
data, err := json.Marshal(bucketPolicy)
if err != nil {
return err
}
// Construct path to policy.json for the given bucket.
configFile := path.Join(bucketConfigPrefix, bucketName, bucketPolicyConfig)
return saveConfig(objAPI, configFile, data)
}
func removePolicyConfig(ctx context.Context, objAPI ObjectLayer, bucketName string) error {
// Construct path to policy.json for the given bucket.
configFile := path.Join(bucketConfigPrefix, bucketName, bucketPolicyConfig)
if err := objAPI.DeleteObject(ctx, minioMetaBucket, configFile); err != nil {
if _, ok := err.(ObjectNotFound); ok {
return BucketPolicyNotFound{Bucket: bucketName}
}
return err
}
return nil
}
// PolicyToBucketAccessPolicy - converts policy.Policy to minio-go/policy.BucketAccessPolicy.
func PolicyToBucketAccessPolicy(bucketPolicy *policy.Policy) (*miniogopolicy.BucketAccessPolicy, error) {
// Return empty BucketAccessPolicy for empty bucket policy.
if bucketPolicy == nil {
return &miniogopolicy.BucketAccessPolicy{Version: policy.DefaultVersion}, nil
}
data, err := json.Marshal(bucketPolicy)
if err != nil {
// This should not happen because bucketPolicy is valid to convert to JSON data.
return nil, err
}
var policyInfo miniogopolicy.BucketAccessPolicy
if err = json.Unmarshal(data, &policyInfo); err != nil {
// This should not happen because data is valid to JSON data.
return nil, err
}
return &policyInfo, nil
}
// BucketAccessPolicyToPolicy - converts minio-go/policy.BucketAccessPolicy to policy.Policy.
func BucketAccessPolicyToPolicy(policyInfo *miniogopolicy.BucketAccessPolicy) (*policy.Policy, error) {
data, err := json.Marshal(policyInfo)
if err != nil {
// This should not happen because policyInfo is valid to convert to JSON data.
return nil, err
}
var bucketPolicy policy.Policy
if err = json.Unmarshal(data, &bucketPolicy); err != nil {
// This should not happen because data is valid to JSON data.
return nil, err
}
return &bucketPolicy, nil
}

424
cmd/policy_test.go Normal file
View File

@ -0,0 +1,424 @@
/*
* 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 cmd
import (
"reflect"
"testing"
miniogopolicy "github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio-go/pkg/set"
"github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/policy/condition"
)
func TestPolicySysSet(t *testing.T) {
case1PolicySys := NewPolicySys()
case1Policy := policy.Policy{
Version: policy.DefaultVersion,
Statements: []policy.Statement{
policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(policy.PutObjectAction),
policy.NewResourceSet(policy.NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
),
},
}
case1Result := NewPolicySys()
case1Result.bucketPolicyMap["mybucket"] = case1Policy
case2PolicySys := NewPolicySys()
case2PolicySys.bucketPolicyMap["mybucket"] = case1Policy
case2Policy := policy.Policy{
Version: policy.DefaultVersion,
Statements: []policy.Statement{
policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(policy.GetObjectAction),
policy.NewResourceSet(policy.NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
),
},
}
case2Result := NewPolicySys()
case2Result.bucketPolicyMap["mybucket"] = case2Policy
case3PolicySys := NewPolicySys()
case3PolicySys.bucketPolicyMap["mybucket"] = case2Policy
case3Policy := policy.Policy{
ID: "MyPolicyForMyBucket",
Version: policy.DefaultVersion,
}
case3Result := NewPolicySys()
testCases := []struct {
policySys *PolicySys
bucketName string
bucketPolicy policy.Policy
expectedResult *PolicySys
}{
{case1PolicySys, "mybucket", case1Policy, case1Result},
{case2PolicySys, "mybucket", case2Policy, case2Result},
{case3PolicySys, "mybucket", case3Policy, case3Result},
}
for i, testCase := range testCases {
result := testCase.policySys
result.Set(testCase.bucketName, testCase.bucketPolicy)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestPolicySysRemove(t *testing.T) {
case1Policy := policy.Policy{
Version: policy.DefaultVersion,
Statements: []policy.Statement{
policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(policy.PutObjectAction),
policy.NewResourceSet(policy.NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
),
},
}
case1PolicySys := NewPolicySys()
case1PolicySys.bucketPolicyMap["mybucket"] = case1Policy
case1Result := NewPolicySys()
case2Policy := policy.Policy{
Version: policy.DefaultVersion,
Statements: []policy.Statement{
policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(policy.GetObjectAction),
policy.NewResourceSet(policy.NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
),
},
}
case2PolicySys := NewPolicySys()
case2PolicySys.bucketPolicyMap["mybucket"] = case2Policy
case2Result := NewPolicySys()
case2Result.bucketPolicyMap["mybucket"] = case2Policy
case3PolicySys := NewPolicySys()
case3Result := NewPolicySys()
testCases := []struct {
policySys *PolicySys
bucketName string
expectedResult *PolicySys
}{
{case1PolicySys, "mybucket", case1Result},
{case2PolicySys, "yourbucket", case2Result},
{case3PolicySys, "mybucket", case3Result},
}
for i, testCase := range testCases {
result := testCase.policySys
result.Remove(testCase.bucketName)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestPolicySysIsAllowed(t *testing.T) {
policySys := NewPolicySys()
policySys.Set("mybucket", policy.Policy{
Version: policy.DefaultVersion,
Statements: []policy.Statement{
policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(policy.GetBucketLocationAction),
policy.NewResourceSet(policy.NewResource("mybucket", "")),
condition.NewFunctions(),
),
policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(policy.PutObjectAction),
policy.NewResourceSet(policy.NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
),
},
})
anonGetBucketLocationArgs := policy.Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: policy.GetBucketLocationAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{},
}
anonPutObjectActionArgs := policy.Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: policy.PutObjectAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{
"x-amz-copy-source": {"mybucket/myobject"},
"SourceIp": {"192.168.1.10"},
},
ObjectName: "myobject",
}
anonGetObjectActionArgs := policy.Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: policy.GetObjectAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{},
ObjectName: "myobject",
}
getBucketLocationArgs := policy.Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: policy.GetBucketLocationAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{},
IsOwner: true,
}
putObjectActionArgs := policy.Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: policy.PutObjectAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{
"x-amz-copy-source": {"mybucket/myobject"},
"SourceIp": {"192.168.1.10"},
},
IsOwner: true,
ObjectName: "myobject",
}
getObjectActionArgs := policy.Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: policy.GetObjectAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{},
IsOwner: true,
ObjectName: "myobject",
}
yourbucketAnonGetObjectActionArgs := policy.Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: policy.GetObjectAction,
BucketName: "yourbucket",
ConditionValues: map[string][]string{},
ObjectName: "yourobject",
}
yourbucketGetObjectActionArgs := policy.Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: policy.GetObjectAction,
BucketName: "yourbucket",
ConditionValues: map[string][]string{},
IsOwner: true,
ObjectName: "yourobject",
}
testCases := []struct {
policySys *PolicySys
args policy.Args
expectedResult bool
}{
{policySys, anonGetBucketLocationArgs, true},
{policySys, anonPutObjectActionArgs, true},
{policySys, anonGetObjectActionArgs, false},
{policySys, getBucketLocationArgs, true},
{policySys, putObjectActionArgs, true},
{policySys, getObjectActionArgs, true},
{policySys, yourbucketAnonGetObjectActionArgs, false},
{policySys, yourbucketGetObjectActionArgs, true},
}
for i, testCase := range testCases {
result := testCase.policySys.IsAllowed(testCase.args)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func getReadOnlyStatement(bucketName, prefix string) []miniogopolicy.Statement {
return []miniogopolicy.Statement{
{
Effect: string(policy.Allow),
Principal: miniogopolicy.User{AWS: set.CreateStringSet("*")},
Resources: set.CreateStringSet(policy.NewResource(bucketName, "").String()),
Actions: set.CreateStringSet("s3:GetBucketLocation", "s3:ListBucket"),
},
{
Effect: string(policy.Allow),
Principal: miniogopolicy.User{AWS: set.CreateStringSet("*")},
Resources: set.CreateStringSet(policy.NewResource(bucketName, prefix).String()),
Actions: set.CreateStringSet("s3:GetObject"),
},
}
}
func TestPolicyToBucketAccessPolicy(t *testing.T) {
case1Policy := &policy.Policy{
Version: policy.DefaultVersion,
Statements: []policy.Statement{
policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(policy.GetBucketLocationAction, policy.ListBucketAction),
policy.NewResourceSet(policy.NewResource("mybucket", "")),
condition.NewFunctions(),
),
policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(policy.GetObjectAction),
policy.NewResourceSet(policy.NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
),
},
}
case1Result := &miniogopolicy.BucketAccessPolicy{
Version: policy.DefaultVersion,
Statements: getReadOnlyStatement("mybucket", "/myobject*"),
}
case2Policy := &policy.Policy{
Version: policy.DefaultVersion,
Statements: []policy.Statement{},
}
case2Result := &miniogopolicy.BucketAccessPolicy{
Version: policy.DefaultVersion,
Statements: []miniogopolicy.Statement{},
}
case3Policy := &policy.Policy{
Version: "12-10-2012",
Statements: []policy.Statement{
policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(policy.PutObjectAction),
policy.NewResourceSet(policy.NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
),
},
}
testCases := []struct {
bucketPolicy *policy.Policy
expectedResult *miniogopolicy.BucketAccessPolicy
expectErr bool
}{
{case1Policy, case1Result, false},
{case2Policy, case2Result, false},
{case3Policy, nil, true},
}
for i, testCase := range testCases {
result, err := PolicyToBucketAccessPolicy(testCase.bucketPolicy)
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 !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %+v, got: %+v\n", i+1, testCase.expectedResult, result)
}
}
}
}
func TestBucketAccessPolicyToPolicy(t *testing.T) {
case1PolicyInfo := &miniogopolicy.BucketAccessPolicy{
Version: policy.DefaultVersion,
Statements: getReadOnlyStatement("mybucket", "/myobject*"),
}
case1Result := &policy.Policy{
Version: policy.DefaultVersion,
Statements: []policy.Statement{
policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(policy.GetBucketLocationAction, policy.ListBucketAction),
policy.NewResourceSet(policy.NewResource("mybucket", "")),
condition.NewFunctions(),
),
policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(policy.GetObjectAction),
policy.NewResourceSet(policy.NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
),
},
}
case2PolicyInfo := &miniogopolicy.BucketAccessPolicy{
Version: policy.DefaultVersion,
Statements: []miniogopolicy.Statement{},
}
case2Result := &policy.Policy{
Version: policy.DefaultVersion,
Statements: []policy.Statement{},
}
case3PolicyInfo := &miniogopolicy.BucketAccessPolicy{
Version: "12-10-2012",
Statements: getReadOnlyStatement("mybucket", "/myobject*"),
}
testCases := []struct {
policyInfo *miniogopolicy.BucketAccessPolicy
expectedResult *policy.Policy
expectErr bool
}{
{case1PolicyInfo, case1Result, false},
{case2PolicyInfo, case2Result, false},
{case3PolicyInfo, nil, true},
}
for i, testCase := range testCases {
result, err := BucketAccessPolicyToPolicy(testCase.policyInfo)
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 !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %+v, got: %+v\n", i+1, testCase.expectedResult, result)
}
}
}
}

View File

@ -239,9 +239,12 @@ func serverMain(ctx *cli.Context) {
handler, err = configureServerHandler(globalEndpoints) handler, err = configureServerHandler(globalEndpoints)
logger.FatalIf(err, "Unable to configure one of server's RPC services.") logger.FatalIf(err, "Unable to configure one of server's RPC services.")
// Initialize notification system. // Create new notification system.
globalNotificationSys, err = NewNotificationSys(globalServerConfig, globalEndpoints) globalNotificationSys, err = NewNotificationSys(globalServerConfig, globalEndpoints)
logger.FatalIf(err, "Unable to initialize notification system.") logger.FatalIf(err, "Unable to create new notification system.")
// Create new policy system.
globalPolicySys = NewPolicySys()
// Initialize Admin Peers inter-node communication only in distributed setup. // Initialize Admin Peers inter-node communication only in distributed setup.
initGlobalAdminPeers(globalEndpoints) initGlobalAdminPeers(globalEndpoints)

View File

@ -36,6 +36,7 @@ import (
"time" "time"
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
"github.com/minio/minio/pkg/policy"
) )
// API suite container common to both FS and XL. // API suite container common to both FS and XL.
@ -319,7 +320,7 @@ func (s *TestSuiteCommon) TestBucketSQSNotificationAMQP(c *check) {
// Deletes the policy and verifies the deletion by fetching it back. // Deletes the policy and verifies the deletion by fetching it back.
func (s *TestSuiteCommon) TestBucketPolicy(c *check) { func (s *TestSuiteCommon) TestBucketPolicy(c *check) {
// Sample bucket policy. // Sample bucket policy.
bucketPolicyBuf := `{"Version":"2012-10-17","Statement":[{"Action":["s3:GetBucketLocation","s3:ListBucket"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s"],"Sid":""},{"Action":["s3:GetObject"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s/this*"],"Sid":""}]}` bucketPolicyBuf := `{"Version":"2012-10-17","Statement":[{"Action":["s3:GetBucketLocation","s3:ListBucket"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s"]},{"Action":["s3:GetObject"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s/this*"]}]}`
// generate a random bucket Name. // generate a random bucket Name.
bucketName := getRandomBucketName() bucketName := getRandomBucketName()
@ -361,7 +362,11 @@ func (s *TestSuiteCommon) TestBucketPolicy(c *check) {
bucketPolicyReadBuf, err := ioutil.ReadAll(response.Body) bucketPolicyReadBuf, err := ioutil.ReadAll(response.Body)
c.Assert(err, nil) c.Assert(err, nil)
// Verify if downloaded policy matches with previousy uploaded. // Verify if downloaded policy matches with previousy uploaded.
c.Assert(bytes.Equal([]byte(bucketPolicyStr), bucketPolicyReadBuf), true) expectedPolicy, err := policy.ParseConfig(strings.NewReader(bucketPolicyStr), bucketName)
c.Assert(err, nil)
gotPolicy, err := policy.ParseConfig(bytes.NewReader(bucketPolicyReadBuf), bucketName)
c.Assert(err, nil)
c.Assert(reflect.DeepEqual(expectedPolicy, gotPolicy), true)
// Delete policy. // Delete policy.
request, err = newTestSignedRequest("DELETE", getDeletePolicyURL(s.endPoint, bucketName), 0, nil, request, err = newTestSignedRequest("DELETE", getDeletePolicyURL(s.endPoint, bucketName), 0, nil,

View File

@ -53,12 +53,12 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio-go/pkg/s3signer" "github.com/minio/minio-go/pkg/s3signer"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/bpool" "github.com/minio/minio/pkg/bpool"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/policy"
) )
// Tests should initNSLock only once. // Tests should initNSLock only once.
@ -354,9 +354,12 @@ func UnstartedTestServer(t TestErrHandler, instanceType string) TestServer {
globalMinioAddr = getEndpointsLocalAddr(testServer.Disks) globalMinioAddr = getEndpointsLocalAddr(testServer.Disks)
globalNotificationSys, err = NewNotificationSys(globalServerConfig, testServer.Disks) globalNotificationSys, err = NewNotificationSys(globalServerConfig, testServer.Disks)
if err != nil { if err != nil {
t.Fatalf("Unable to initialize queue configuration") t.Fatalf("Unable to create new notification system. %v", err)
} }
// Create new policy system.
globalPolicySys = NewPolicySys()
return testServer return testServer
} }
@ -1715,17 +1718,14 @@ func newTestObjectLayer(endpoints EndpointList) (newObject ObjectLayer, err erro
return xl.storageDisks return xl.storageDisks
} }
// Initialize and load bucket policies. // Create new notification system.
xl.bucketPolicies, err = initBucketPolicies(xl)
if err != nil {
return nil, err
}
// Initialize a new event notifier.
if globalNotificationSys, err = NewNotificationSys(globalServerConfig, endpoints); err != nil { if globalNotificationSys, err = NewNotificationSys(globalServerConfig, endpoints); err != nil {
return nil, err return nil, err
} }
// Create new policy system.
globalPolicySys = NewPolicySys()
return xl, nil return xl, nil
} }
@ -1821,7 +1821,7 @@ func prepareTestBackend(instanceType string) (ObjectLayer, []string, error) {
// STEP 2: Set the policy to allow the unsigned request, use the policyFunc to obtain the relevant statement and call // STEP 2: Set the policy to allow the unsigned request, use the policyFunc to obtain the relevant statement and call
// the handler again to verify its success. // the handler again to verify its success.
func ExecObjectLayerAPIAnonTest(t *testing.T, obj ObjectLayer, testName, bucketName, objectName, instanceType string, apiRouter http.Handler, func ExecObjectLayerAPIAnonTest(t *testing.T, obj ObjectLayer, testName, bucketName, objectName, instanceType string, apiRouter http.Handler,
anonReq *http.Request, policyFunc func(string, string) policy.Statement) { anonReq *http.Request, bucketPolicy *policy.Policy) {
anonTestStr := "Anonymous HTTP request test" anonTestStr := "Anonymous HTTP request test"
unknownSignTestStr := "Unknown HTTP signature test" unknownSignTestStr := "Unknown HTTP signature test"
@ -1863,7 +1863,8 @@ func ExecObjectLayerAPIAnonTest(t *testing.T, obj ObjectLayer, testName, bucketN
// HEAD HTTTP request doesn't contain response body. // HEAD HTTTP request doesn't contain response body.
if anonReq.Method != "HEAD" { if anonReq.Method != "HEAD" {
// read the response body. // read the response body.
actualContent, err := ioutil.ReadAll(rec.Body) var actualContent []byte
actualContent, err = ioutil.ReadAll(rec.Body)
if err != nil { if err != nil {
t.Fatal(failTestStr(anonTestStr, fmt.Sprintf("Failed parsing response body: <ERROR> %v", err))) t.Fatal(failTestStr(anonTestStr, fmt.Sprintf("Failed parsing response body: <ERROR> %v", err)))
} }
@ -1872,13 +1873,13 @@ func ExecObjectLayerAPIAnonTest(t *testing.T, obj ObjectLayer, testName, bucketN
t.Fatal(failTestStr(anonTestStr, "error response content differs from expected value")) t.Fatal(failTestStr(anonTestStr, "error response content differs from expected value"))
} }
} }
// Set write only policy on bucket to allow anonymous HTTP request for the operation under test.
// request to go through. if err := obj.SetBucketPolicy(context.Background(), bucketName, bucketPolicy); err != nil {
bp := policy.BucketAccessPolicy{ t.Fatalf("unexpected error. %v", err)
Version: "1.0",
Statements: []policy.Statement{policyFunc(bucketName, "")},
} }
obj.SetBucketPolicy(context.Background(), bucketName, bp) globalPolicySys.Set(bucketName, *bucketPolicy)
defer globalPolicySys.Remove(bucketName)
// now call the handler again with the unsigned/anonymous request, it should be accepted. // now call the handler again with the unsigned/anonymous request, it should be accepted.
rec = httptest.NewRecorder() rec = httptest.NewRecorder()

View File

@ -33,12 +33,13 @@ import (
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/gorilla/rpc/v2/json2" "github.com/gorilla/rpc/v2/json2"
"github.com/minio/minio-go/pkg/policy" miniogopolicy "github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio/browser" "github.com/minio/minio/browser"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/policy"
) )
// WebGenericArgs - empty struct for calls that don't accept arguments // WebGenericArgs - empty struct for calls that don't accept arguments
@ -155,15 +156,24 @@ func (web *webAPIHandlers) DeleteBucket(r *http.Request, args *RemoveBucketArgs,
return toJSONError(errAuthentication) return toJSONError(errAuthentication)
} }
ctx := context.Background()
deleteBucket := objectAPI.DeleteBucket deleteBucket := objectAPI.DeleteBucket
if web.CacheAPI() != nil { if web.CacheAPI() != nil {
deleteBucket = web.CacheAPI().DeleteBucket deleteBucket = web.CacheAPI().DeleteBucket
} }
err := deleteBucket(context.Background(), args.BucketName)
if err != nil { if err := deleteBucket(ctx, args.BucketName); err != nil {
return toJSONError(err, args.BucketName) return toJSONError(err, args.BucketName)
} }
globalNotificationSys.RemoveNotification(args.BucketName)
globalPolicySys.Remove(args.BucketName)
for addr, err := range globalNotificationSys.DeleteBucket(args.BucketName) {
logger.GetReqInfo(ctx).AppendTags("remotePeer", addr.Name)
logger.LogIf(ctx, err)
}
reply.UIVersion = browser.UIVersion reply.UIVersion = browser.UIVersion
return nil return nil
} }
@ -249,26 +259,37 @@ func (web *webAPIHandlers) ListObjects(r *http.Request, args *ListObjectsArgs, r
if web.CacheAPI() != nil { if web.CacheAPI() != nil {
listObjects = web.CacheAPI().ListObjects listObjects = web.CacheAPI().ListObjects
} }
prefix := args.Prefix + "test" // To test if GetObject/PutObject with the specified prefix is allowed.
readable := isBucketActionAllowed("s3:GetObject", args.BucketName, prefix, objectAPI) // Check if anonymous (non-owner) has access to download objects.
writable := isBucketActionAllowed("s3:PutObject", args.BucketName, prefix, objectAPI) readable := globalPolicySys.IsAllowed(policy.Args{
authErr := webRequestAuthenticate(r) Action: policy.GetObjectAction,
switch { BucketName: args.BucketName,
case authErr == errAuthentication: ConditionValues: getConditionValues(r, ""),
return toJSONError(authErr) IsOwner: false,
case authErr == nil: ObjectName: args.Prefix + "/",
break })
case readable && writable: // Check if anonymous (non-owner) has access to upload objects.
reply.Writable = true writable := globalPolicySys.IsAllowed(policy.Args{
break Action: policy.PutObjectAction,
case readable: BucketName: args.BucketName,
break ConditionValues: getConditionValues(r, ""),
case writable: IsOwner: false,
reply.Writable = true ObjectName: args.Prefix + "/",
return nil })
default:
return errAuthentication if authErr := webRequestAuthenticate(r); authErr != nil {
if authErr == errAuthentication {
return toJSONError(authErr)
}
// Error out anonymous (non-owner) has no access download or upload objects.
if !readable && !writable {
return errAuthentication
}
reply.Writable = writable
} }
lo, err := listObjects(context.Background(), args.BucketName, args.Prefix, args.Marker, slashSeparator, 1000) lo, err := listObjects(context.Background(), args.BucketName, args.Prefix, args.Marker, slashSeparator, 1000)
if err != nil { if err != nil {
return &json2.Error{Message: err.Error()} return &json2.Error{Message: err.Error()}
@ -556,14 +577,23 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
bucket := vars["bucket"] bucket := vars["bucket"]
object := vars["object"] object := vars["object"]
authErr := webRequestAuthenticate(r) if authErr := webRequestAuthenticate(r); authErr != nil {
if authErr == errAuthentication { if authErr == errAuthentication {
writeWebErrorResponse(w, errAuthentication) writeWebErrorResponse(w, errAuthentication)
return return
} }
if authErr != nil && !isBucketActionAllowed("s3:PutObject", bucket, object, objectAPI) {
writeWebErrorResponse(w, errAuthentication) // Check if anonymous (non-owner) has access to upload objects.
return if !globalPolicySys.IsAllowed(policy.Args{
Action: policy.PutObjectAction,
BucketName: bucket,
ConditionValues: getConditionValues(r, ""),
IsOwner: false,
ObjectName: object,
}) {
writeWebErrorResponse(w, errAuthentication)
return
}
} }
// Require Content-Length to be set in the request // Require Content-Length to be set in the request
@ -614,9 +644,18 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
object := vars["object"] object := vars["object"]
token := r.URL.Query().Get("token") token := r.URL.Query().Get("token")
if !isAuthTokenValid(token) && !isBucketActionAllowed("s3:GetObject", bucket, object, objectAPI) { if !isAuthTokenValid(token) {
writeWebErrorResponse(w, errAuthentication) // Check if anonymous (non-owner) has access to download objects.
return if !globalPolicySys.IsAllowed(policy.Args{
Action: policy.GetObjectAction,
BucketName: bucket,
ConditionValues: getConditionValues(r, ""),
IsOwner: false,
ObjectName: object,
}) {
writeWebErrorResponse(w, errAuthentication)
return
}
} }
getObject := objectAPI.GetObject getObject := objectAPI.GetObject
@ -669,7 +708,14 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token") token := r.URL.Query().Get("token")
if !isAuthTokenValid(token) { if !isAuthTokenValid(token) {
for _, object := range args.Objects { for _, object := range args.Objects {
if !isBucketActionAllowed("s3:GetObject", args.BucketName, pathJoin(args.Prefix, object), objectAPI) { // Check if anonymous (non-owner) has access to download objects.
if !globalPolicySys.IsAllowed(policy.Args{
Action: policy.GetObjectAction,
BucketName: args.BucketName,
ConditionValues: getConditionValues(r, ""),
IsOwner: false,
ObjectName: pathJoin(args.Prefix, object),
}) {
writeWebErrorResponse(w, errAuthentication) writeWebErrorResponse(w, errAuthentication)
return return
} }
@ -742,8 +788,8 @@ type GetBucketPolicyArgs struct {
// GetBucketPolicyRep - get bucket policy reply. // GetBucketPolicyRep - get bucket policy reply.
type GetBucketPolicyRep struct { type GetBucketPolicyRep struct {
UIVersion string `json:"uiVersion"` UIVersion string `json:"uiVersion"`
Policy policy.BucketPolicy `json:"policy"` Policy miniogopolicy.BucketPolicy `json:"policy"`
} }
// GetBucketPolicy - get bucket policy for the requested prefix. // GetBucketPolicy - get bucket policy for the requested prefix.
@ -757,16 +803,21 @@ func (web *webAPIHandlers) GetBucketPolicy(r *http.Request, args *GetBucketPolic
return toJSONError(errAuthentication) return toJSONError(errAuthentication)
} }
var policyInfo, err = objectAPI.GetBucketPolicy(context.Background(), args.BucketName) bucketPolicy, err := objectAPI.GetBucketPolicy(context.Background(), args.BucketName)
if err != nil { if err != nil {
_, ok := err.(BucketPolicyNotFound) if _, ok := err.(BucketPolicyNotFound); !ok {
if !ok {
return toJSONError(err, args.BucketName) return toJSONError(err, args.BucketName)
} }
} }
policyInfo, err := PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
// This should not happen.
return toJSONError(err, args.BucketName)
}
reply.UIVersion = browser.UIVersion reply.UIVersion = browser.UIVersion
reply.Policy = policy.GetPolicy(policyInfo.Statements, args.BucketName, args.Prefix) reply.Policy = miniogopolicy.GetPolicy(policyInfo.Statements, args.BucketName, args.Prefix)
return nil return nil
} }
@ -778,9 +829,9 @@ type ListAllBucketPoliciesArgs struct {
// BucketAccessPolicy - Collection of canned bucket policy at a given prefix. // BucketAccessPolicy - Collection of canned bucket policy at a given prefix.
type BucketAccessPolicy struct { type BucketAccessPolicy struct {
Bucket string `json:"bucket"` Bucket string `json:"bucket"`
Prefix string `json:"prefix"` Prefix string `json:"prefix"`
Policy policy.BucketPolicy `json:"policy"` Policy miniogopolicy.BucketPolicy `json:"policy"`
} }
// ListAllBucketPoliciesRep - get all bucket policy reply. // ListAllBucketPoliciesRep - get all bucket policy reply.
@ -789,7 +840,7 @@ type ListAllBucketPoliciesRep struct {
Policies []BucketAccessPolicy `json:"policies"` Policies []BucketAccessPolicy `json:"policies"`
} }
// GetllBucketPolicy - get all bucket policy. // ListAllBucketPolicies - get all bucket policy.
func (web *webAPIHandlers) ListAllBucketPolicies(r *http.Request, args *ListAllBucketPoliciesArgs, reply *ListAllBucketPoliciesRep) error { func (web *webAPIHandlers) ListAllBucketPolicies(r *http.Request, args *ListAllBucketPoliciesArgs, reply *ListAllBucketPoliciesRep) error {
objectAPI := web.ObjectAPI() objectAPI := web.ObjectAPI()
if objectAPI == nil { if objectAPI == nil {
@ -799,15 +850,22 @@ func (web *webAPIHandlers) ListAllBucketPolicies(r *http.Request, args *ListAllB
if !isHTTPRequestValid(r) { if !isHTTPRequestValid(r) {
return toJSONError(errAuthentication) return toJSONError(errAuthentication)
} }
var policyInfo, err = objectAPI.GetBucketPolicy(context.Background(), args.BucketName)
bucketPolicy, err := objectAPI.GetBucketPolicy(context.Background(), args.BucketName)
if err != nil { if err != nil {
_, ok := err.(PolicyNotFound) if _, ok := err.(BucketPolicyNotFound); !ok {
if !ok {
return toJSONError(err, args.BucketName) return toJSONError(err, args.BucketName)
} }
} }
policyInfo, err := PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
// This should not happen.
return toJSONError(err, args.BucketName)
}
reply.UIVersion = browser.UIVersion reply.UIVersion = browser.UIVersion
for prefix, policy := range policy.GetPolicies(policyInfo.Statements, args.BucketName, "") { for prefix, policy := range miniogopolicy.GetPolicies(policyInfo.Statements, args.BucketName, "") {
bucketName, objectPrefix := urlPath2BucketObjectName(prefix) bucketName, objectPrefix := urlPath2BucketObjectName(prefix)
objectPrefix = strings.TrimSuffix(objectPrefix, "*") objectPrefix = strings.TrimSuffix(objectPrefix, "*")
reply.Policies = append(reply.Policies, BucketAccessPolicy{ reply.Policies = append(reply.Policies, BucketAccessPolicy{
@ -816,18 +874,19 @@ func (web *webAPIHandlers) ListAllBucketPolicies(r *http.Request, args *ListAllB
Policy: policy, Policy: policy,
}) })
} }
return nil return nil
} }
// SetBucketPolicyArgs - set bucket policy args. // SetBucketPolicyWebArgs - set bucket policy args.
type SetBucketPolicyArgs struct { type SetBucketPolicyWebArgs struct {
BucketName string `json:"bucketName"` BucketName string `json:"bucketName"`
Prefix string `json:"prefix"` Prefix string `json:"prefix"`
Policy string `json:"policy"` Policy string `json:"policy"`
} }
// SetBucketPolicy - set bucket policy. // SetBucketPolicy - set bucket policy.
func (web *webAPIHandlers) SetBucketPolicy(r *http.Request, args *SetBucketPolicyArgs, reply *WebGenericRep) error { func (web *webAPIHandlers) SetBucketPolicy(r *http.Request, args *SetBucketPolicyWebArgs, reply *WebGenericRep) error {
objectAPI := web.ObjectAPI() objectAPI := web.ObjectAPI()
reply.UIVersion = browser.UIVersion reply.UIVersion = browser.UIVersion
@ -839,52 +898,56 @@ func (web *webAPIHandlers) SetBucketPolicy(r *http.Request, args *SetBucketPolic
return toJSONError(errAuthentication) return toJSONError(errAuthentication)
} }
bucketP := policy.BucketPolicy(args.Policy) policyType := miniogopolicy.BucketPolicy(args.Policy)
if !bucketP.IsValidBucketPolicy() { if !policyType.IsValidBucketPolicy() {
return &json2.Error{ return &json2.Error{
Message: "Invalid policy type " + args.Policy, Message: "Invalid policy type " + args.Policy,
} }
} }
var policyInfo, err = objectAPI.GetBucketPolicy(context.Background(), args.BucketName) ctx := context.Background()
bucketPolicy, err := objectAPI.GetBucketPolicy(ctx, args.BucketName)
if err != nil { if err != nil {
if _, ok := err.(PolicyNotFound); !ok { if _, ok := err.(BucketPolicyNotFound); !ok {
return toJSONError(err, args.BucketName) return toJSONError(err, args.BucketName)
} }
policyInfo = policy.BucketAccessPolicy{Version: "2012-10-17"}
} }
policyInfo.Statements = policy.SetPolicy(policyInfo.Statements, bucketP, args.BucketName, args.Prefix) policyInfo, err := PolicyToBucketAccessPolicy(bucketPolicy)
if err != nil {
// This should not happen.
return toJSONError(err, args.BucketName)
}
policyInfo.Statements = miniogopolicy.SetPolicy(policyInfo.Statements, policyType, args.BucketName, args.Prefix)
if len(policyInfo.Statements) == 0 { if len(policyInfo.Statements) == 0 {
if err = objectAPI.DeleteBucketPolicy(context.Background(), args.BucketName); err != nil { if err = objectAPI.DeleteBucketPolicy(ctx, args.BucketName); err != nil {
return toJSONError(err, args.BucketName) return toJSONError(err, args.BucketName)
} }
globalPolicySys.Remove(args.BucketName)
return nil return nil
} }
_, err = json.Marshal(policyInfo) bucketPolicy, err = BucketAccessPolicyToPolicy(policyInfo)
if err != nil { if err != nil {
return toJSONError(err) // This should not happen.
}
// Parse check bucket policy.
if s3Error := checkBucketPolicyResources(args.BucketName, policyInfo); s3Error != ErrNone {
apiErr := getAPIError(s3Error)
var err error
if apiErr.Code == "XMinioPolicyNesting" {
err = PolicyNesting{}
} else {
err = fmt.Errorf(apiErr.Description)
}
return toJSONError(err, args.BucketName) return toJSONError(err, args.BucketName)
} }
// Parse validate and save bucket policy. // Parse validate and save bucket policy.
if err := objectAPI.SetBucketPolicy(context.Background(), args.BucketName, policyInfo); err != nil { if err := objectAPI.SetBucketPolicy(ctx, args.BucketName, bucketPolicy); err != nil {
return toJSONError(err, args.BucketName) return toJSONError(err, args.BucketName)
} }
globalPolicySys.Set(args.BucketName, *bucketPolicy)
for addr, err := range globalNotificationSys.SetBucketPolicy(args.BucketName, bucketPolicy) {
logger.GetReqInfo(ctx).AppendTags("remotePeer", addr.Name)
logger.LogIf(ctx, err)
}
return nil return nil
} }

View File

@ -37,9 +37,10 @@ import (
jwtgo "github.com/dgrijalva/jwt-go" jwtgo "github.com/dgrijalva/jwt-go"
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
"github.com/minio/minio-go/pkg/policy" miniogopolicy "github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio-go/pkg/set"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/policy/condition"
) )
// Implement a dummy flush writer. // Implement a dummy flush writer.
@ -555,12 +556,22 @@ func testListObjectsWebHandler(obj ObjectLayer, instanceType string, t TestErrHa
t.Fatalf("Expected error `%s`", err) t.Fatalf("Expected error `%s`", err)
} }
policy := policy.BucketAccessPolicy{ bucketPolicy := &policy.Policy{
Version: "1.0", Version: policy.DefaultVersion,
Statements: []policy.Statement{getReadOnlyObjectStatement(bucketName, "")}, Statements: []policy.Statement{policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(policy.GetObjectAction),
policy.NewResourceSet(policy.NewResource(bucketName, "*")),
condition.NewFunctions(),
)},
} }
obj.SetBucketPolicy(context.Background(), bucketName, policy) if err = obj.SetBucketPolicy(context.Background(), bucketName, bucketPolicy); err != nil {
t.Fatalf("unexpected error. %v", err)
}
globalPolicySys.Set(bucketName, *bucketPolicy)
defer globalPolicySys.Remove(bucketName)
// Unauthenticated ListObjects with READ bucket policy should succeed. // Unauthenticated ListObjects with READ bucket policy should succeed.
err, reply = test("") err, reply = test("")
@ -929,12 +940,22 @@ func testUploadWebHandler(obj ObjectLayer, instanceType string, t TestErrHandler
t.Fatalf("Expected the response status to be 403, but instead found `%d`", code) t.Fatalf("Expected the response status to be 403, but instead found `%d`", code)
} }
bp := policy.BucketAccessPolicy{ bucketPolicy := &policy.Policy{
Version: "1.0", Version: policy.DefaultVersion,
Statements: []policy.Statement{getWriteOnlyObjectStatement(bucketName, "")}, Statements: []policy.Statement{policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(policy.PutObjectAction),
policy.NewResourceSet(policy.NewResource(bucketName, "*")),
condition.NewFunctions(),
)},
} }
obj.SetBucketPolicy(context.Background(), bucketName, bp) if err := obj.SetBucketPolicy(context.Background(), bucketName, bucketPolicy); err != nil {
t.Fatalf("unexpected error. %v", err)
}
globalPolicySys.Set(bucketName, *bucketPolicy)
defer globalPolicySys.Remove(bucketName)
// Unauthenticated upload with WRITE policy should succeed. // Unauthenticated upload with WRITE policy should succeed.
code = test("", true) code = test("", true)
@ -1036,12 +1057,22 @@ func testDownloadWebHandler(obj ObjectLayer, instanceType string, t TestErrHandl
t.Fatalf("Expected the response status to be 403, but instead found `%d`", code) t.Fatalf("Expected the response status to be 403, but instead found `%d`", code)
} }
bp := policy.BucketAccessPolicy{ bucketPolicy := &policy.Policy{
Version: "1.0", Version: policy.DefaultVersion,
Statements: []policy.Statement{getReadOnlyObjectStatement(bucketName, "")}, Statements: []policy.Statement{policy.NewStatement(
policy.Allow,
policy.NewPrincipal("*"),
policy.NewActionSet(policy.GetObjectAction),
policy.NewResourceSet(policy.NewResource(bucketName, "*")),
condition.NewFunctions(),
)},
} }
obj.SetBucketPolicy(context.Background(), bucketName, bp) if err := obj.SetBucketPolicy(context.Background(), bucketName, bucketPolicy); err != nil {
t.Fatalf("unexpected error. %v", err)
}
globalPolicySys.Set(bucketName, *bucketPolicy)
defer globalPolicySys.Remove(bucketName)
// Unauthenticated download with READ policy should succeed. // Unauthenticated download with READ policy should succeed.
code, bodyContent = test("") code, bodyContent = test("")
@ -1260,43 +1291,40 @@ func testWebGetBucketPolicyHandler(obj ObjectLayer, instanceType string, t TestE
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
bucketName := getRandomBucketName() bucketName := getRandomBucketName()
if err := obj.MakeBucketWithLocation(context.Background(), bucketName, ""); err != nil { if err = obj.MakeBucketWithLocation(context.Background(), bucketName, ""); err != nil {
t.Fatal("Unexpected error: ", err) t.Fatal("Unexpected error: ", err)
} }
policyVal := policy.BucketAccessPolicy{ bucketPolicy := &policy.Policy{
Version: "2012-10-17", Version: policy.DefaultVersion,
Statements: []policy.Statement{ Statements: []policy.Statement{
{ policy.NewStatement(
Actions: set.CreateStringSet("s3:GetBucketLocation", "s3:ListBucket"), policy.Allow,
Effect: "Allow", policy.NewPrincipal("*"),
Principal: policy.User{ policy.NewActionSet(policy.GetBucketLocationAction, policy.ListBucketAction),
AWS: set.CreateStringSet("*"), policy.NewResourceSet(policy.NewResource(bucketName, "")),
}, condition.NewFunctions(),
Resources: set.CreateStringSet(bucketARNPrefix + bucketName), ),
Sid: "", policy.NewStatement(
}, policy.Allow,
{ policy.NewPrincipal("*"),
Actions: set.CreateStringSet("s3:GetObject"), policy.NewActionSet(policy.GetObjectAction),
Effect: "Allow", policy.NewResourceSet(policy.NewResource(bucketName, "*")),
Principal: policy.User{ condition.NewFunctions(),
AWS: set.CreateStringSet("*"), ),
},
Resources: set.CreateStringSet(bucketARNPrefix + bucketName + "/*"),
Sid: "",
},
}, },
} }
if err := writeBucketPolicy(context.Background(), bucketName, obj, policyVal); err != nil {
if err = savePolicyConfig(obj, bucketName, bucketPolicy); err != nil {
t.Fatal("Unexpected error: ", err) t.Fatal("Unexpected error: ", err)
} }
testCases := []struct { testCases := []struct {
bucketName string bucketName string
prefix string prefix string
expectedResult policy.BucketPolicy expectedResult miniogopolicy.BucketPolicy
}{ }{
{bucketName, "", policy.BucketPolicyReadOnly}, {bucketName, "", miniogopolicy.BucketPolicyReadOnly},
} }
for i, testCase := range testCases { for i, testCase := range testCases {
@ -1338,57 +1366,63 @@ func testWebListAllBucketPoliciesHandler(obj ObjectLayer, instanceType string, t
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
bucketName := getRandomBucketName() bucketName := getRandomBucketName()
if err := obj.MakeBucketWithLocation(context.Background(), bucketName, ""); err != nil { if err = obj.MakeBucketWithLocation(context.Background(), bucketName, ""); err != nil {
t.Fatal("Unexpected error: ", err) t.Fatal("Unexpected error: ", err)
} }
stringEqualsConditions := policy.ConditionMap{} func1, err := condition.NewStringEqualsFunc(condition.S3Prefix, "hello")
stringEqualsConditions["StringEquals"] = make(policy.ConditionKeyMap) if err != nil {
stringEqualsConditions["StringEquals"].Add("s3:prefix", set.CreateStringSet("hello")) t.Fatalf("Unable to create string equals condition function. %v", err)
}
policyVal := policy.BucketAccessPolicy{ bucketPolicy := &policy.Policy{
Version: "2012-10-17", Version: policy.DefaultVersion,
Statements: []policy.Statement{ Statements: []policy.Statement{
{ policy.NewStatement(
Actions: set.CreateStringSet("s3:GetBucketLocation"), policy.Allow,
Effect: "Allow", policy.NewPrincipal("*"),
Principal: policy.User{AWS: set.CreateStringSet("*")}, policy.NewActionSet(policy.GetBucketLocationAction),
Resources: set.CreateStringSet(bucketARNPrefix + bucketName), policy.NewResourceSet(policy.NewResource(bucketName, "")),
Sid: "", condition.NewFunctions(),
}, ),
{ policy.NewStatement(
Actions: set.CreateStringSet("s3:ListBucket"), policy.Allow,
Conditions: stringEqualsConditions, policy.NewPrincipal("*"),
Effect: "Allow", policy.NewActionSet(policy.ListBucketAction),
Principal: policy.User{AWS: set.CreateStringSet("*")}, policy.NewResourceSet(policy.NewResource(bucketName, "")),
Resources: set.CreateStringSet(bucketARNPrefix + bucketName), condition.NewFunctions(func1),
Sid: "", ),
}, policy.NewStatement(
{ policy.Allow,
Actions: set.CreateStringSet("s3:ListBucketMultipartUploads"), policy.NewPrincipal("*"),
Effect: "Allow", policy.NewActionSet(policy.ListBucketMultipartUploadsAction),
Principal: policy.User{AWS: set.CreateStringSet("*")}, policy.NewResourceSet(policy.NewResource(bucketName, "")),
Resources: set.CreateStringSet(bucketARNPrefix + bucketName), condition.NewFunctions(),
Sid: "", ),
}, policy.NewStatement(
{ policy.Allow,
Actions: set.CreateStringSet("s3:AbortMultipartUpload", "s3:DeleteObject", policy.NewPrincipal("*"),
"s3:GetObject", "s3:ListMultipartUploadParts", "s3:PutObject"), policy.NewActionSet(
Effect: "Allow", policy.AbortMultipartUploadAction,
Principal: policy.User{AWS: set.CreateStringSet("*")}, policy.DeleteObjectAction,
Resources: set.CreateStringSet(bucketARNPrefix + bucketName + "/hello*"), policy.GetObjectAction,
Sid: "", policy.ListMultipartUploadPartsAction,
}, policy.PutObjectAction,
),
policy.NewResourceSet(policy.NewResource(bucketName, "hello*")),
condition.NewFunctions(),
),
}, },
} }
if err := writeBucketPolicy(context.Background(), bucketName, obj, policyVal); err != nil {
if err = savePolicyConfig(obj, bucketName, bucketPolicy); err != nil {
t.Fatal("Unexpected error: ", err) t.Fatal("Unexpected error: ", err)
} }
testCaseResult1 := []BucketAccessPolicy{{ testCaseResult1 := []BucketAccessPolicy{{
Bucket: bucketName, Bucket: bucketName,
Prefix: "hello", Prefix: "hello",
Policy: policy.BucketPolicyReadWrite, Policy: miniogopolicy.BucketPolicyReadWrite,
}} }}
testCases := []struct { testCases := []struct {
bucketName string bucketName string
@ -1460,7 +1494,7 @@ func testWebSetBucketPolicyHandler(obj ObjectLayer, instanceType string, t TestE
} }
for i, testCase := range testCases { for i, testCase := range testCases {
args := &SetBucketPolicyArgs{BucketName: testCase.bucketName, Prefix: testCase.prefix, Policy: testCase.policy} args := &SetBucketPolicyWebArgs{BucketName: testCase.bucketName, Prefix: testCase.prefix, Policy: testCase.policy}
reply := &WebGenericRep{} reply := &WebGenericRep{}
// Call SetBucketPolicy RPC // Call SetBucketPolicy RPC
req, err := newTestWebRPCRequest("Web.SetBucketPolicy", authorization, args) req, err := newTestWebRPCRequest("Web.SetBucketPolicy", authorization, args)
@ -1719,7 +1753,7 @@ func TestWebObjectLayerFaultyDisks(t *testing.T) {
{"ListBuckets", AuthRPCArgs{Version: globalRPCAPIVersion}, ListBucketsRep{}}, {"ListBuckets", AuthRPCArgs{Version: globalRPCAPIVersion}, ListBucketsRep{}},
{"ListObjects", ListObjectsArgs{BucketName: bucketName, Prefix: ""}, ListObjectsRep{}}, {"ListObjects", ListObjectsArgs{BucketName: bucketName, Prefix: ""}, ListObjectsRep{}},
{"GetBucketPolicy", GetBucketPolicyArgs{BucketName: bucketName, Prefix: ""}, GetBucketPolicyRep{}}, {"GetBucketPolicy", GetBucketPolicyArgs{BucketName: bucketName, Prefix: ""}, GetBucketPolicyRep{}},
{"SetBucketPolicy", SetBucketPolicyArgs{BucketName: bucketName, Prefix: "", Policy: "none"}, WebGenericRep{}}, {"SetBucketPolicy", SetBucketPolicyWebArgs{BucketName: bucketName, Prefix: "", Policy: "none"}, WebGenericRep{}},
} }
for _, rpcCall := range webRPCs { for _, rpcCall := range webRPCs {

View File

@ -21,17 +21,16 @@ import (
"fmt" "fmt"
"hash/crc32" "hash/crc32"
"io" "io"
"reflect"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/bpool" "github.com/minio/minio/pkg/bpool"
"github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/hash"
"github.com/minio/minio/pkg/madmin" "github.com/minio/minio/pkg/madmin"
"github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/sync/errgroup" "github.com/minio/minio/pkg/sync/errgroup"
) )
@ -77,9 +76,6 @@ type xlSets struct {
// Distribution algorithm of choice. // Distribution algorithm of choice.
distributionAlgo string distributionAlgo string
// Variable represents bucket policies in memory.
bucketPolicies *bucketPolicies
// Pack level listObjects pool management. // Pack level listObjects pool management.
listPool *treeWalkPool listPool *treeWalkPool
} }
@ -263,16 +259,14 @@ func newXLSets(endpoints EndpointList, format *formatXLV3, setCount int, drivesP
// Connect disks right away. // Connect disks right away.
s.connectDisks() s.connectDisks()
// Initialize and load bucket policies. // Initialize notification system.
var err error if err := globalNotificationSys.Init(s); err != nil {
s.bucketPolicies, err = initBucketPolicies(s) return nil, fmt.Errorf("Unable to initialize notification system. %v", err)
if err != nil {
return nil, err
} }
// Initialize notification system. // Initialize policy system.
if err = globalNotificationSys.Init(s); err != nil { if err := globalPolicySys.Init(s); err != nil {
return nil, fmt.Errorf("Unable to initialize event notification. %s", err) return nil, fmt.Errorf("Unable to initialize policy system. %v", err)
} }
// Start the disk monitoring and connect routine. // Start the disk monitoring and connect routine.
@ -473,35 +467,18 @@ func (s *xlSets) ListObjectsV2(ctx context.Context, bucket, prefix, continuation
} }
// SetBucketPolicy persist the new policy on the bucket. // SetBucketPolicy persist the new policy on the bucket.
func (s *xlSets) SetBucketPolicy(ctx context.Context, bucket string, policy policy.BucketAccessPolicy) error { func (s *xlSets) SetBucketPolicy(ctx context.Context, bucket string, policy *policy.Policy) error {
return persistAndNotifyBucketPolicyChange(ctx, bucket, false, policy, s) return savePolicyConfig(s, bucket, policy)
} }
// GetBucketPolicy will return a policy on a bucket // GetBucketPolicy will return a policy on a bucket
func (s *xlSets) GetBucketPolicy(ctx context.Context, bucket string) (policy.BucketAccessPolicy, error) { func (s *xlSets) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
// fetch bucket policy from cache. return GetPolicyConfig(s, bucket)
bpolicy := s.bucketPolicies.GetBucketPolicy(bucket)
if reflect.DeepEqual(bpolicy, emptyBucketPolicy) {
return ReadBucketPolicy(bucket, s)
}
return bpolicy, nil
} }
// DeleteBucketPolicy deletes all policies on bucket // DeleteBucketPolicy deletes all policies on bucket
func (s *xlSets) DeleteBucketPolicy(ctx context.Context, bucket string) error { func (s *xlSets) DeleteBucketPolicy(ctx context.Context, bucket string) error {
return persistAndNotifyBucketPolicyChange(ctx, bucket, true, emptyBucketPolicy, s) return removePolicyConfig(ctx, s, bucket)
}
// RefreshBucketPolicy refreshes policy cache from disk
func (s *xlSets) RefreshBucketPolicy(ctx context.Context, bucket string) error {
policy, err := ReadBucketPolicy(bucket, s)
if err != nil {
if reflect.DeepEqual(policy, emptyBucketPolicy) {
return s.bucketPolicies.DeleteBucketPolicy(bucket)
}
return err
}
return s.bucketPolicies.SetBucketPolicy(bucket, policy)
} }
// IsNotificationSupported returns whether bucket notification is applicable for this layer. // IsNotificationSupported returns whether bucket notification is applicable for this layer.

View File

@ -18,12 +18,11 @@ package cmd
import ( import (
"context" "context"
"reflect"
"sort" "sort"
"sync" "sync"
"github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio/cmd/logger" "github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/policy"
) )
// list all errors that can be ignore in a bucket operation. // list all errors that can be ignore in a bucket operation.
@ -280,36 +279,18 @@ func (xl xlObjects) DeleteBucket(ctx context.Context, bucket string) error {
} }
// SetBucketPolicy sets policy on bucket // SetBucketPolicy sets policy on bucket
func (xl xlObjects) SetBucketPolicy(ctx context.Context, bucket string, policy policy.BucketAccessPolicy) error { func (xl xlObjects) SetBucketPolicy(ctx context.Context, bucket string, policy *policy.Policy) error {
return persistAndNotifyBucketPolicyChange(ctx, bucket, false, policy, xl) return savePolicyConfig(xl, bucket, policy)
} }
// GetBucketPolicy will get policy on bucket // GetBucketPolicy will get policy on bucket
func (xl xlObjects) GetBucketPolicy(ctx context.Context, bucket string) (policy.BucketAccessPolicy, error) { func (xl xlObjects) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
// fetch bucket policy from cache. return GetPolicyConfig(xl, bucket)
bpolicy := xl.bucketPolicies.GetBucketPolicy(bucket)
if reflect.DeepEqual(bpolicy, emptyBucketPolicy) {
return ReadBucketPolicy(bucket, xl)
}
return bpolicy, nil
} }
// DeleteBucketPolicy deletes all policies on bucket // DeleteBucketPolicy deletes all policies on bucket
func (xl xlObjects) DeleteBucketPolicy(ctx context.Context, bucket string) error { func (xl xlObjects) DeleteBucketPolicy(ctx context.Context, bucket string) error {
return persistAndNotifyBucketPolicyChange(ctx, bucket, true, emptyBucketPolicy, xl) return removePolicyConfig(ctx, xl, bucket)
}
// RefreshBucketPolicy refreshes policy cache from disk
func (xl xlObjects) RefreshBucketPolicy(ctx context.Context, bucket string) error {
policy, err := ReadBucketPolicy(bucket, xl)
if err != nil {
if reflect.DeepEqual(policy, emptyBucketPolicy) {
return xl.bucketPolicies.DeleteBucketPolicy(bucket)
}
return err
}
return xl.bucketPolicies.SetBucketPolicy(bucket, policy)
} }
// IsNotificationSupported returns whether bucket notification is applicable for this layer. // IsNotificationSupported returns whether bucket notification is applicable for this layer.

View File

@ -43,9 +43,6 @@ type xlObjects struct {
// Byte pools used for temporary i/o buffers. // Byte pools used for temporary i/o buffers.
bp *bpool.BytePoolCap bp *bpool.BytePoolCap
// Variable represents bucket policies in memory.
bucketPolicies *bucketPolicies
// TODO: Deprecated only kept here for tests, should be removed in future. // TODO: Deprecated only kept here for tests, should be removed in future.
storageDisks []StorageAPI storageDisks []StorageAPI

267
pkg/policy/action.go Normal file
View File

@ -0,0 +1,267 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
"github.com/minio/minio/pkg/policy/condition"
)
// Action - policy action.
// Refer https://docs.aws.amazon.com/IAM/latest/UserGuide/list_s3.html
// for more information about available actions.
type Action string
const (
// AbortMultipartUploadAction - AbortMultipartUpload Rest API action.
AbortMultipartUploadAction Action = "s3:AbortMultipartUpload"
// CreateBucketAction - CreateBucket Rest API action.
CreateBucketAction = "s3:CreateBucket"
// DeleteBucketAction - DeleteBucket Rest API action.
DeleteBucketAction = "s3:DeleteBucket"
// DeleteBucketPolicyAction - DeleteBucketPolicy Rest API action.
DeleteBucketPolicyAction = "s3:DeleteBucketPolicy"
// DeleteObjectAction - DeleteObject Rest API action.
DeleteObjectAction = "s3:DeleteObject"
// GetBucketLocationAction - GetBucketLocation Rest API action.
GetBucketLocationAction = "s3:GetBucketLocation"
// GetBucketNotificationAction - GetBucketNotification Rest API action.
GetBucketNotificationAction = "s3:GetBucketNotification"
// GetBucketPolicyAction - GetBucketPolicy Rest API action.
GetBucketPolicyAction = "s3:GetBucketPolicy"
// GetObjectAction - GetObject Rest API action.
GetObjectAction = "s3:GetObject"
// HeadBucketAction - HeadBucket Rest API action. This action is unused in minio.
HeadBucketAction = "s3:HeadBucket"
// ListAllMyBucketsAction - ListAllMyBuckets (List buckets) Rest API action.
ListAllMyBucketsAction = "s3:ListAllMyBuckets"
// ListBucketAction - ListBucket Rest API action.
ListBucketAction = "s3:ListBucket"
// ListBucketMultipartUploadsAction - ListMultipartUploads Rest API action.
ListBucketMultipartUploadsAction = "s3:ListBucketMultipartUploads"
// ListenBucketNotificationAction - ListenBucketNotification Rest API action.
// This is Minio extension.
ListenBucketNotificationAction = "s3:ListenBucketNotification"
// ListMultipartUploadPartsAction - ListParts Rest API action.
ListMultipartUploadPartsAction = "s3:ListMultipartUploadParts"
// ListObjectsAction - ListObjects Rest API action exactly same behavior as ListBucketAction.
ListObjectsAction = "s3:ListObjects"
// PutBucketNotificationAction - PutObjectNotification Rest API action.
PutBucketNotificationAction = "s3:PutBucketNotification"
// PutBucketPolicyAction - PutBucketPolicy Rest API action.
PutBucketPolicyAction = "s3:PutBucketPolicy"
// PutObjectAction - PutObject Rest API action.
PutObjectAction = "s3:PutObject"
)
// isObjectAction - returns whether action is object type or not.
func (action Action) isObjectAction() bool {
switch action {
case AbortMultipartUploadAction, DeleteObjectAction, GetObjectAction:
fallthrough
case ListMultipartUploadPartsAction, PutObjectAction:
return true
}
return false
}
// IsValid - checks if action is valid or not.
func (action Action) IsValid() bool {
switch action {
case AbortMultipartUploadAction, CreateBucketAction, DeleteBucketAction:
fallthrough
case DeleteBucketPolicyAction, DeleteObjectAction, GetBucketLocationAction:
fallthrough
case GetBucketNotificationAction, GetBucketPolicyAction, GetObjectAction:
fallthrough
case HeadBucketAction, ListAllMyBucketsAction, ListBucketAction:
fallthrough
case ListBucketMultipartUploadsAction, ListenBucketNotificationAction:
fallthrough
case ListMultipartUploadPartsAction, ListObjectsAction, PutBucketNotificationAction:
fallthrough
case PutBucketPolicyAction, PutObjectAction:
return true
}
return false
}
// MarshalJSON - encodes Action to JSON data.
func (action Action) MarshalJSON() ([]byte, error) {
if action.IsValid() {
return json.Marshal(string(action))
}
return nil, fmt.Errorf("invalid action '%v'", action)
}
// UnmarshalJSON - decodes JSON data to Action.
func (action *Action) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
a := Action(s)
if !a.IsValid() {
return fmt.Errorf("invalid action '%v'", s)
}
*action = a
return nil
}
func parseAction(s string) (Action, error) {
action := Action(s)
if action.IsValid() {
return action, nil
}
return action, fmt.Errorf("unsupported action '%v'", s)
}
// actionConditionKeyMap - holds mapping of supported condition key for an action.
var actionConditionKeyMap = map[Action]condition.KeySet{
AbortMultipartUploadAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
CreateBucketAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
DeleteBucketPolicyAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
DeleteObjectAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
GetBucketLocationAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
GetBucketNotificationAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
GetBucketPolicyAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
GetObjectAction: condition.NewKeySet(
condition.S3XAmzServerSideEncryption,
condition.S3XAmzServerSideEncryptionAwsKMSKeyID,
condition.S3XAmzStorageClass,
condition.AWSReferer,
condition.AWSSourceIP,
),
HeadBucketAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
ListAllMyBucketsAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
ListBucketAction: condition.NewKeySet(
condition.S3Prefix,
condition.S3Delimiter,
condition.S3MaxKeys,
condition.AWSReferer,
condition.AWSSourceIP,
),
ListBucketMultipartUploadsAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
ListenBucketNotificationAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
ListMultipartUploadPartsAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
ListObjectsAction: condition.NewKeySet(
condition.S3Prefix,
condition.S3Delimiter,
condition.S3MaxKeys,
condition.AWSReferer,
condition.AWSSourceIP,
),
PutBucketNotificationAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
PutBucketPolicyAction: condition.NewKeySet(
condition.AWSReferer,
condition.AWSSourceIP,
),
PutObjectAction: condition.NewKeySet(
condition.S3XAmzCopySource,
condition.S3XAmzServerSideEncryption,
condition.S3XAmzServerSideEncryptionAwsKMSKeyID,
condition.S3XAmzMetadataDirective,
condition.S3XAmzStorageClass,
condition.AWSReferer,
condition.AWSSourceIP,
),
}

116
pkg/policy/action_test.go Normal file
View File

@ -0,0 +1,116 @@
/*
* 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 policy
import (
"encoding/json"
"reflect"
"testing"
)
func TestActionIsObjectAction(t *testing.T) {
testCases := []struct {
action Action
expectedResult bool
}{
{AbortMultipartUploadAction, true},
{DeleteObjectAction, true},
{GetObjectAction, true},
{ListMultipartUploadPartsAction, true},
{PutObjectAction, true},
{CreateBucketAction, false},
}
for i, testCase := range testCases {
result := testCase.action.isObjectAction()
if testCase.expectedResult != result {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestActionIsValid(t *testing.T) {
testCases := []struct {
action Action
expectedResult bool
}{
{AbortMultipartUploadAction, true},
{Action("foo"), false},
}
for i, testCase := range testCases {
result := testCase.action.IsValid()
if testCase.expectedResult != result {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestActionMarshalJSON(t *testing.T) {
testCases := []struct {
action Action
expectedResult []byte
expectErr bool
}{
{PutObjectAction, []byte(`"s3:PutObject"`), false},
{Action("foo"), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.action)
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, testCase.expectedResult, result)
}
}
}
}
func TestActionUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult Action
expectErr bool
}{
{[]byte(`"s3:PutObject"`), PutObjectAction, false},
{[]byte(`"foo"`), Action(""), true},
}
for i, testCase := range testCases {
var result Action
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)
}
}
}
}

114
pkg/policy/actionset.go Normal file
View File

@ -0,0 +1,114 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
"sort"
"github.com/minio/minio-go/pkg/set"
)
// ActionSet - set of actions.
type ActionSet map[Action]struct{}
// Add - add action to the set.
func (actionSet ActionSet) Add(action Action) {
actionSet[action] = struct{}{}
}
// Contains - checks given action exists in the action set.
func (actionSet ActionSet) Contains(action Action) bool {
_, found := actionSet[action]
return found
}
// Intersection - returns actions available in both ActionSet.
func (actionSet ActionSet) Intersection(sset ActionSet) ActionSet {
nset := NewActionSet()
for k := range actionSet {
if _, ok := sset[k]; ok {
nset.Add(k)
}
}
return nset
}
// MarshalJSON - encodes ActionSet to JSON data.
func (actionSet ActionSet) MarshalJSON() ([]byte, error) {
if len(actionSet) == 0 {
return nil, fmt.Errorf("empty action set")
}
return json.Marshal(actionSet.ToSlice())
}
func (actionSet ActionSet) String() string {
actions := []string{}
for action := range actionSet {
actions = append(actions, string(action))
}
sort.Strings(actions)
return fmt.Sprintf("%v", actions)
}
// ToSlice - returns slice of actions from the action set.
func (actionSet ActionSet) ToSlice() []Action {
actions := []Action{}
for action := range actionSet {
actions = append(actions, action)
}
return actions
}
// UnmarshalJSON - decodes JSON data to ActionSet.
func (actionSet *ActionSet) UnmarshalJSON(data []byte) error {
var sset set.StringSet
if err := json.Unmarshal(data, &sset); err != nil {
return err
}
if len(sset) == 0 {
return fmt.Errorf("empty action set")
}
*actionSet = make(ActionSet)
for _, s := range sset.ToSlice() {
action, err := parseAction(s)
if err != nil {
return err
}
actionSet.Add(action)
}
return nil
}
// NewActionSet - creates new action set.
func NewActionSet(actions ...Action) ActionSet {
actionSet := make(ActionSet)
for _, action := range actions {
actionSet.Add(action)
}
return actionSet
}

View File

@ -0,0 +1,158 @@
/*
* 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 policy
import (
"encoding/json"
"reflect"
"testing"
)
func TestActionSetAdd(t *testing.T) {
testCases := []struct {
set ActionSet
action Action
expectedResult ActionSet
}{
{NewActionSet(), PutObjectAction, NewActionSet(PutObjectAction)},
{NewActionSet(PutObjectAction), PutObjectAction, NewActionSet(PutObjectAction)},
}
for i, testCase := range testCases {
testCase.set.Add(testCase.action)
if !reflect.DeepEqual(testCase.expectedResult, testCase.set) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set)
}
}
}
func TestActionSetContains(t *testing.T) {
testCases := []struct {
set ActionSet
action Action
expectedResult bool
}{
{NewActionSet(PutObjectAction), PutObjectAction, true},
{NewActionSet(PutObjectAction, GetObjectAction), PutObjectAction, true},
{NewActionSet(PutObjectAction, GetObjectAction), AbortMultipartUploadAction, false},
}
for i, testCase := range testCases {
result := testCase.set.Contains(testCase.action)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestActionSetIntersection(t *testing.T) {
testCases := []struct {
set ActionSet
setToIntersect ActionSet
expectedResult ActionSet
}{
{NewActionSet(), NewActionSet(PutObjectAction), NewActionSet()},
{NewActionSet(PutObjectAction), NewActionSet(), NewActionSet()},
{NewActionSet(PutObjectAction), NewActionSet(PutObjectAction, GetObjectAction), NewActionSet(PutObjectAction)},
}
for i, testCase := range testCases {
result := testCase.set.Intersection(testCase.setToIntersect)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set)
}
}
}
func TestActionSetMarshalJSON(t *testing.T) {
testCases := []struct {
actionSet ActionSet
expectedResult []byte
expectErr bool
}{
{NewActionSet(PutObjectAction), []byte(`["s3:PutObject"]`), false},
{NewActionSet(), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.actionSet)
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 !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, string(testCase.expectedResult), string(result))
}
}
}
}
func TestActionSetToSlice(t *testing.T) {
testCases := []struct {
actionSet ActionSet
expectedResult []Action
}{
{NewActionSet(PutObjectAction), []Action{PutObjectAction}},
{NewActionSet(), []Action{}},
}
for i, testCase := range testCases {
result := testCase.actionSet.ToSlice()
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestActionSetUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult ActionSet
expectErr bool
}{
{[]byte(`"s3:PutObject"`), NewActionSet(PutObjectAction), false},
{[]byte(`["s3:PutObject"]`), NewActionSet(PutObjectAction), false},
{[]byte(`["s3:PutObject", "s3:GetObject"]`), NewActionSet(PutObjectAction, GetObjectAction), false},
{[]byte(`["s3:PutObject", "s3:GetObject", "s3:PutObject"]`), NewActionSet(PutObjectAction, GetObjectAction), false},
{[]byte(`[]`), NewActionSet(), true}, // Empty array.
{[]byte(`"foo"`), nil, true}, // Invalid action.
{[]byte(`["s3:PutObject", "foo"]`), nil, true}, // Invalid action.
}
for i, testCase := range testCases {
result := make(ActionSet)
err := json.Unmarshal(testCase.data, &result)
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 !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}

View File

@ -0,0 +1,168 @@
/*
* 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 condition
import (
"encoding/json"
"fmt"
"sort"
)
// Function - condition function interface.
type Function interface {
// evaluate() - evaluates this condition function with given values.
evaluate(values map[string][]string) bool
// key() - returns condition key used in this function.
key() Key
// name() - returns condition name of this function.
name() name
// String() - returns string representation of function.
String() string
// toMap - returns map representation of this function.
toMap() map[Key]ValueSet
}
// Functions - list of functions.
type Functions []Function
// Evaluate - evaluates all functions with given values map. Each function is evaluated
// sequencely and next function is called only if current function succeeds.
func (functions Functions) Evaluate(values map[string][]string) bool {
for _, f := range functions {
if !f.evaluate(values) {
return false
}
}
return true
}
// Keys - returns list of keys used in all functions.
func (functions Functions) Keys() KeySet {
keySet := NewKeySet()
for _, f := range functions {
keySet.Add(f.key())
}
return keySet
}
// MarshalJSON - encodes Functions to JSON data.
func (functions Functions) MarshalJSON() ([]byte, error) {
nm := make(map[name]map[Key]ValueSet)
for _, f := range functions {
nm[f.name()] = f.toMap()
}
return json.Marshal(nm)
}
func (functions Functions) String() string {
funcStrings := []string{}
for _, f := range functions {
s := fmt.Sprintf("%v", f)
funcStrings = append(funcStrings, s)
}
sort.Strings(funcStrings)
return fmt.Sprintf("%v", funcStrings)
}
// UnmarshalJSON - decodes JSON data to Functions.
func (functions *Functions) UnmarshalJSON(data []byte) error {
// As string kind, int kind then json.Unmarshaler is checked at
// https://github.com/golang/go/blob/master/src/encoding/json/decode.go#L618
// UnmarshalJSON() is not called for types extending string
// see https://play.golang.org/p/HrSsKksHvrS, better way to do is
// https://play.golang.org/p/y9ElWpBgVAB
//
// Due to this issue, name and Key types cannot be used as map keys below.
nm := make(map[string]map[string]ValueSet)
if err := json.Unmarshal(data, &nm); err != nil {
return err
}
if len(nm) == 0 {
return fmt.Errorf("condition must not be empty")
}
funcs := []Function{}
for nameString, args := range nm {
n, err := parseName(nameString)
if err != nil {
return err
}
for keyString, values := range args {
key, err := parseKey(keyString)
if err != nil {
return err
}
var f Function
switch n {
case stringEquals:
if f, err = newStringEqualsFunc(key, values); err != nil {
return err
}
case stringNotEquals:
if f, err = newStringNotEqualsFunc(key, values); err != nil {
return err
}
case stringLike:
if f, err = newStringLikeFunc(key, values); err != nil {
return err
}
case stringNotLike:
if f, err = newStringNotLikeFunc(key, values); err != nil {
return err
}
case ipAddress:
if f, err = newIPAddressFunc(key, values); err != nil {
return err
}
case notIPAddress:
if f, err = newNotIPAddressFunc(key, values); err != nil {
return err
}
case null:
if f, err = newNullFunc(key, values); err != nil {
return err
}
default:
return fmt.Errorf("%v is not handled", n)
}
funcs = append(funcs, f)
}
}
*functions = funcs
return nil
}
// NewFunctions - returns new Functions with given function list.
func NewFunctions(functions ...Function) Functions {
return Functions(functions)
}

View File

@ -0,0 +1,298 @@
/*
* 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 condition
import (
"encoding/json"
"reflect"
"testing"
)
func TestFunctionsEvaluate(t *testing.T) {
func1, err := newNullFunc(S3XAmzCopySource, NewValueSet(NewBoolValue(true)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func2, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func3, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func4, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case1Function := NewFunctions(func1, func2, func3, func4)
testCases := []struct {
functions Functions
values map[string][]string
expectedResult bool
}{
{case1Function, map[string][]string{
"x-amz-copy-source": {"mybucket/myobject"},
"SourceIp": {"192.168.1.10"},
}, true},
{case1Function, map[string][]string{
"x-amz-copy-source": {"mybucket/myobject"},
"SourceIp": {"192.168.1.10"},
"Refer": {"http://example.org/"},
}, true},
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, false},
{case1Function, map[string][]string{"SourceIp": {"192.168.1.10"}}, false},
{case1Function, map[string][]string{
"x-amz-copy-source": {"mybucket/yourobject"},
"SourceIp": {"192.168.1.10"},
}, false},
{case1Function, map[string][]string{
"x-amz-copy-source": {"mybucket/myobject"},
"SourceIp": {"192.168.2.10"},
}, false},
{case1Function, map[string][]string{
"x-amz-copy-source": {"mybucket/myobject"},
"Refer": {"http://example.org/"},
}, false},
}
for i, testCase := range testCases {
result := testCase.functions.Evaluate(testCase.values)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestFunctionsKeys(t *testing.T) {
func1, err := newNullFunc(S3XAmzCopySource, NewValueSet(NewBoolValue(true)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func2, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func3, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func4, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
functions Functions
expectedResult KeySet
}{
{NewFunctions(func1, func2, func3, func4), NewKeySet(S3XAmzCopySource, AWSSourceIP)},
}
for i, testCase := range testCases {
result := testCase.functions.Keys()
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestFunctionsMarshalJSON(t *testing.T) {
func1, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func2, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func3, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func4, err := newNotIPAddressFunc(AWSSourceIP,
NewValueSet(NewStringValue("10.1.10.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func5, err := newStringNotLikeFunc(S3XAmzStorageClass, NewValueSet(NewStringValue("STANDARD")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func6, err := newNullFunc(S3XAmzServerSideEncryptionAwsKMSKeyID, NewValueSet(NewBoolValue(true)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func7, err := newIPAddressFunc(AWSSourceIP,
NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case1Result := []byte(`{"IpAddress":{"aws:SourceIp":["192.168.1.0/24"]},"NotIpAddress":{"aws:SourceIp":["10.1.10.0/24"]},"Null":{"s3:x-amz-server-side-encryption-aws-kms-key-id":[true]},"StringEquals":{"s3:x-amz-copy-source":["mybucket/myobject"]},"StringLike":{"s3:x-amz-metadata-directive":["REPL*"]},"StringNotEquals":{"s3:x-amz-server-side-encryption":["AES256"]},"StringNotLike":{"s3:x-amz-storage-class":["STANDARD"]}}`)
case2Result := []byte(`{"Null":{"s3:x-amz-server-side-encryption-aws-kms-key-id":[true]}}`)
testCases := []struct {
functions Functions
expectedResult []byte
expectErr bool
}{
{NewFunctions(func1, func2, func3, func4, func5, func6, func7), case1Result, false},
{NewFunctions(func6), case2Result, false},
{NewFunctions(), []byte(`{}`), false},
{nil, []byte(`{}`), false},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.functions)
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 TestFunctionsUnmarshalJSON(t *testing.T) {
case1Data := []byte(`{
"StringLike": {
"s3:x-amz-metadata-directive": "REPL*"
},
"StringEquals": {
"s3:x-amz-copy-source": "mybucket/myobject"
},
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "AES256"
},
"NotIpAddress": {
"aws:SourceIp": [
"10.1.10.0/24",
"10.10.1.0/24"
]
},
"StringNotLike": {
"s3:x-amz-storage-class": "STANDARD"
},
"Null": {
"s3:x-amz-server-side-encryption-aws-kms-key-id": true
},
"IpAddress": {
"aws:SourceIp": [
"192.168.1.0/24",
"192.168.2.0/24"
]
}
}`)
func1, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func2, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func3, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func4, err := newNotIPAddressFunc(AWSSourceIP,
NewValueSet(NewStringValue("10.1.10.0/24"), NewStringValue("10.10.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func5, err := newStringNotLikeFunc(S3XAmzStorageClass, NewValueSet(NewStringValue("STANDARD")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func6, err := newNullFunc(S3XAmzServerSideEncryptionAwsKMSKeyID, NewValueSet(NewBoolValue(true)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func7, err := newIPAddressFunc(AWSSourceIP,
NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("192.168.2.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Data := []byte(`{
"Null": {
"s3:x-amz-server-side-encryption-aws-kms-key-id": true
},
"Null": {
"s3:x-amz-server-side-encryption-aws-kms-key-id": "true"
}
}`)
case3Data := []byte(`{}`)
testCases := []struct {
data []byte
expectedResult Functions
expectErr bool
}{
{case1Data, NewFunctions(func1, func2, func3, func4, func5, func6, func7), false},
// duplicate condition error.
{case2Data, NewFunctions(func6), false},
// empty condition error.
{case3Data, nil, true},
}
for i, testCase := range testCases {
result := new(Functions)
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 (*result).String() != testCase.expectedResult.String() {
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, *result)
}
}
}
}

View File

@ -0,0 +1,180 @@
/*
* 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 condition
import (
"fmt"
"net"
"sort"
)
func toIPAddressFuncString(n name, key Key, values []*net.IPNet) string {
valueStrings := []string{}
for _, value := range values {
valueStrings = append(valueStrings, value.String())
}
sort.Strings(valueStrings)
return fmt.Sprintf("%v:%v:%v", n, key, valueStrings)
}
// ipAddressFunc - IP address function. It checks whether value by Key in given
// values is in IP network. Here Key must be AWSSourceIP.
// For example,
// - if values = [192.168.1.0/24], at evaluate() it returns whether IP address
// in value map for AWSSourceIP falls in the network 192.168.1.10/24.
type ipAddressFunc struct {
k Key
values []*net.IPNet
}
// evaluate() - evaluates to check whether IP address in values map for AWSSourceIP
// falls in one of network or not.
func (f ipAddressFunc) evaluate(values map[string][]string) bool {
IPs := []net.IP{}
for _, s := range values[f.k.Name()] {
IP := net.ParseIP(s)
if IP == nil {
panic(fmt.Errorf("invalid IP address '%v'", s))
}
IPs = append(IPs, IP)
}
for _, IP := range IPs {
for _, IPNet := range f.values {
if IPNet.Contains(IP) {
return true
}
}
}
return false
}
// key() - returns condition key which is used by this condition function.
// Key is always AWSSourceIP.
func (f ipAddressFunc) key() Key {
return f.k
}
// name() - returns "IpAddress" condition name.
func (f ipAddressFunc) name() name {
return ipAddress
}
func (f ipAddressFunc) String() string {
return toIPAddressFuncString(ipAddress, f.k, f.values)
}
// toMap - returns map representation of this function.
func (f ipAddressFunc) toMap() map[Key]ValueSet {
if !f.k.IsValid() {
return nil
}
values := NewValueSet()
for _, value := range f.values {
values.Add(NewStringValue(value.String()))
}
return map[Key]ValueSet{
f.k: values,
}
}
// notIPAddressFunc - Not IP address function. It checks whether value by Key in given
// values is NOT in IP network. Here Key must be AWSSourceIP.
// For example,
// - if values = [192.168.1.0/24], at evaluate() it returns whether IP address
// in value map for AWSSourceIP does not fall in the network 192.168.1.10/24.
type notIPAddressFunc struct {
ipAddressFunc
}
// evaluate() - evaluates to check whether IP address in values map for AWSSourceIP
// does not fall in one of network.
func (f notIPAddressFunc) evaluate(values map[string][]string) bool {
return !f.ipAddressFunc.evaluate(values)
}
// name() - returns "NotIpAddress" condition name.
func (f notIPAddressFunc) name() name {
return notIPAddress
}
func (f notIPAddressFunc) String() string {
return toIPAddressFuncString(notIPAddress, f.ipAddressFunc.k, f.ipAddressFunc.values)
}
func valuesToIPNets(n name, values ValueSet) ([]*net.IPNet, error) {
IPNets := []*net.IPNet{}
for v := range values {
s, err := v.GetString()
if err != nil {
return nil, fmt.Errorf("value %v must be string representation of CIDR for %v condition", v, n)
}
var IPNet *net.IPNet
_, IPNet, err = net.ParseCIDR(s)
if err != nil {
return nil, fmt.Errorf("value %v must be CIDR string for %v condition", s, n)
}
IPNets = append(IPNets, IPNet)
}
return IPNets, nil
}
// newIPAddressFunc - returns new IP address function.
func newIPAddressFunc(key Key, values ValueSet) (Function, error) {
IPNets, err := valuesToIPNets(ipAddress, values)
if err != nil {
return nil, err
}
return NewIPAddressFunc(key, IPNets...)
}
// NewIPAddressFunc - returns new IP address function.
func NewIPAddressFunc(key Key, IPNets ...*net.IPNet) (Function, error) {
if key != AWSSourceIP {
return nil, fmt.Errorf("only %v key is allowed for %v condition", AWSSourceIP, ipAddress)
}
return &ipAddressFunc{key, IPNets}, nil
}
// newNotIPAddressFunc - returns new Not IP address function.
func newNotIPAddressFunc(key Key, values ValueSet) (Function, error) {
IPNets, err := valuesToIPNets(notIPAddress, values)
if err != nil {
return nil, err
}
return NewNotIPAddressFunc(key, IPNets...)
}
// NewNotIPAddressFunc - returns new Not IP address function.
func NewNotIPAddressFunc(key Key, IPNets ...*net.IPNet) (Function, error) {
if key != AWSSourceIP {
return nil, fmt.Errorf("only %v key is allowed for %v condition", AWSSourceIP, notIPAddress)
}
return &notIPAddressFunc{ipAddressFunc{key, IPNets}}, nil
}

View File

@ -0,0 +1,278 @@
/*
* 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 condition
import (
"reflect"
"testing"
)
func TestIPAddressFuncEvaluate(t *testing.T) {
case1Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
values map[string][]string
expectedResult bool
}{
{case1Function, map[string][]string{"SourceIp": {"192.168.1.10"}}, true},
{case1Function, map[string][]string{"SourceIp": {"192.168.2.10"}}, false},
{case1Function, map[string][]string{}, false},
{case1Function, map[string][]string{"delimiter": {"/"}}, false},
}
for i, testCase := range testCases {
result := testCase.function.evaluate(testCase.values)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestIPAddressFuncKey(t *testing.T) {
case1Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
expectedResult Key
}{
{case1Function, AWSSourceIP},
}
for i, testCase := range testCases {
result := testCase.function.key()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestIPAddressFuncToMap(t *testing.T) {
case1Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case1Result := map[Key]ValueSet{
AWSSourceIP: NewValueSet(NewStringValue("192.168.1.0/24")),
}
case2Result := map[Key]ValueSet{
AWSSourceIP: NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")),
}
testCases := []struct {
f Function
expectedResult map[Key]ValueSet
}{
{case1Function, case1Result},
{case2Function, case2Result},
{&ipAddressFunc{}, nil},
}
for i, testCase := range testCases {
result := testCase.f.toMap()
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestNotIPAddressFuncEvaluate(t *testing.T) {
case1Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
values map[string][]string
expectedResult bool
}{
{case1Function, map[string][]string{"SourceIp": {"192.168.2.10"}}, true},
{case1Function, map[string][]string{}, true},
{case1Function, map[string][]string{"delimiter": {"/"}}, true},
{case1Function, map[string][]string{"SourceIp": {"192.168.1.10"}}, false},
}
for i, testCase := range testCases {
result := testCase.function.evaluate(testCase.values)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestNotIPAddressFuncKey(t *testing.T) {
case1Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
expectedResult Key
}{
{case1Function, AWSSourceIP},
}
for i, testCase := range testCases {
result := testCase.function.key()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestNotIPAddressFuncToMap(t *testing.T) {
case1Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case1Result := map[Key]ValueSet{
AWSSourceIP: NewValueSet(NewStringValue("192.168.1.0/24")),
}
case2Result := map[Key]ValueSet{
AWSSourceIP: NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")),
}
testCases := []struct {
f Function
expectedResult map[Key]ValueSet
}{
{case1Function, case1Result},
{case2Function, case2Result},
{&notIPAddressFunc{}, nil},
}
for i, testCase := range testCases {
result := testCase.f.toMap()
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestNewIPAddressFunc(t *testing.T) {
case1Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
key Key
values ValueSet
expectedResult Function
expectErr bool
}{
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")), case1Function, false},
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")), case2Function, false},
// Unsupported key error.
{S3Prefix, NewValueSet(NewStringValue("192.168.1.0/24")), nil, true},
// Invalid value error.
{AWSSourceIP, NewValueSet(NewStringValue("node1.example.org")), nil, true},
// Invalid CIDR format error.
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0.0/24")), nil, true},
}
for i, testCase := range testCases {
result, err := newIPAddressFunc(testCase.key, testCase.values)
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.String() != testCase.expectedResult.String() {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}
func TestNewNotIPAddressFunc(t *testing.T) {
case1Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
key Key
values ValueSet
expectedResult Function
expectErr bool
}{
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")), case1Function, false},
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")), case2Function, false},
// Unsupported key error.
{S3Prefix, NewValueSet(NewStringValue("192.168.1.0/24")), nil, true},
// Invalid value error.
{AWSSourceIP, NewValueSet(NewStringValue("node1.example.org")), nil, true},
// Invalid CIDR format error.
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0.0/24")), nil, true},
}
for i, testCase := range testCases {
result, err := newNotIPAddressFunc(testCase.key, testCase.values)
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.String() != testCase.expectedResult.String() {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}

186
pkg/policy/condition/key.go Normal file
View File

@ -0,0 +1,186 @@
/*
* 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 condition
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 (
// S3XAmzCopySource - key representing x-amz-copy-source HTTP header applicable to PutObject API only.
S3XAmzCopySource Key = "s3:x-amz-copy-source"
// S3XAmzServerSideEncryption - key representing x-amz-server-side-encryption HTTP header applicable
// to PutObject API only.
S3XAmzServerSideEncryption = "s3:x-amz-server-side-encryption"
// S3XAmzServerSideEncryptionAwsKMSKeyID - key representing x-amz-server-side-encryption-aws-kms-key-id
// HTTP header applicable to PutObject API only.
S3XAmzServerSideEncryptionAwsKMSKeyID = "s3:x-amz-server-side-encryption-aws-kms-key-id"
// S3XAmzServerSideEncryptionCustomerAlgorithm - key representing
// x-amz-server-side-encryption-customer-algorithm HTTP header applicable to PutObject API only.
S3XAmzServerSideEncryptionCustomerAlgorithm = "s3:x-amz-server-side-encryption-customer-algorithm"
// S3XAmzMetadataDirective - key representing x-amz-metadata-directive HTTP header applicable to
// PutObject API only.
S3XAmzMetadataDirective = "s3:x-amz-metadata-directive"
// S3XAmzStorageClass - key representing x-amz-storage-class HTTP header applicable to PutObject API
// only.
S3XAmzStorageClass = "s3:x-amz-storage-class"
// S3LocationConstraint - key representing LocationConstraint XML tag of CreateBucket API only.
S3LocationConstraint = "s3:LocationConstraint"
// S3Prefix - key representing prefix query parameter of ListBucket API only.
S3Prefix = "s3:prefix"
// S3Delimiter - key representing delimiter query parameter of ListBucket API only.
S3Delimiter = "s3:delimiter"
// S3MaxKeys - key representing max-keys query parameter of ListBucket API only.
S3MaxKeys = "s3:max-keys"
// AWSReferer - key representing Referer header of any API.
AWSReferer = "aws:Referer"
// AWSSourceIP - key representing client's IP address (not intermittent proxies) of any API.
AWSSourceIP = "aws:SourceIp"
)
// IsValid - checks if key is valid or not.
func (key Key) IsValid() bool {
switch key {
case S3XAmzCopySource, S3XAmzServerSideEncryption, S3XAmzServerSideEncryptionAwsKMSKeyID:
fallthrough
case S3XAmzMetadataDirective, S3XAmzStorageClass, S3LocationConstraint, S3Prefix:
fallthrough
case S3Delimiter, S3MaxKeys, AWSReferer, AWSSourceIP:
return true
}
return false
}
// MarshalJSON - 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))
}
// Name - returns key name 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:")
}
// UnmarshalJSON - 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("invalid condition key '%v'", s)
}
// KeySet - set representation of slice of keys.
type KeySet map[Key]struct{}
// Add - add a key to key set.
func (set KeySet) Add(key Key) {
set[key] = struct{}{}
}
// Difference - returns a key set contains difference of two keys.
// 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
}
// IsEmpty - 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
}
// NewKeySet - 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,214 @@
/*
* 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 condition
import (
"encoding/json"
"reflect"
"testing"
)
func TestKeyIsValid(t *testing.T) {
testCases := []struct {
key Key
expectedResult bool
}{
{S3XAmzCopySource, true},
{S3XAmzServerSideEncryption, true},
{S3XAmzServerSideEncryptionAwsKMSKeyID, true},
{S3XAmzMetadataDirective, true},
{S3XAmzStorageClass, true},
{S3LocationConstraint, true},
{S3Prefix, true},
{S3Delimiter, true},
{S3MaxKeys, true},
{AWSReferer, true},
{AWSSourceIP, true},
{Key("foo"), false},
}
for i, testCase := range testCases {
result := testCase.key.IsValid()
if testCase.expectedResult != result {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestKeyMarshalJSON(t *testing.T) {
testCases := []struct {
key Key
expectedResult []byte
expectErr bool
}{
{S3XAmzCopySource, []byte(`"s3:x-amz-copy-source"`), false},
{Key("foo"), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.key)
expectErr := (err != nil)
if testCase.expectErr != expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: key: expected: %v, got: %v\n", i+1, string(testCase.expectedResult), string(result))
}
}
}
}
func TestKeyName(t *testing.T) {
testCases := []struct {
key Key
expectedResult string
}{
{S3XAmzCopySource, "x-amz-copy-source"},
{AWSReferer, "Referer"},
}
for i, testCase := range testCases {
result := testCase.key.Name()
if testCase.expectedResult != result {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestKeyUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedKey Key
expectErr bool
}{
{[]byte(`"s3:x-amz-copy-source"`), S3XAmzCopySource, false},
{[]byte(`"foo"`), Key(""), true},
}
for i, testCase := range testCases {
var key Key
err := json.Unmarshal(testCase.data, &key)
expectErr := (err != nil)
if testCase.expectErr != expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
}
if !testCase.expectErr {
if testCase.expectedKey != key {
t.Fatalf("case %v: key: expected: %v, got: %v\n", i+1, testCase.expectedKey, key)
}
}
}
}
func TestKeySetAdd(t *testing.T) {
testCases := []struct {
set KeySet
key Key
expectedResult KeySet
}{
{NewKeySet(), S3XAmzCopySource, NewKeySet(S3XAmzCopySource)},
{NewKeySet(S3XAmzCopySource), S3XAmzCopySource, NewKeySet(S3XAmzCopySource)},
}
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(S3XAmzCopySource), NewKeySet()},
{NewKeySet(S3Prefix, S3Delimiter, S3MaxKeys), NewKeySet(S3Delimiter, S3MaxKeys), NewKeySet(S3Prefix)},
}
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(S3Delimiter), 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(S3Delimiter), `[s3:delimiter]`},
}
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(S3Delimiter), []Key{S3Delimiter}},
}
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)
}
}
}

View File

@ -0,0 +1,79 @@
/*
* 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 condition
import (
"encoding/json"
"fmt"
)
type name string
const (
stringEquals name = "StringEquals"
stringNotEquals = "StringNotEquals"
stringLike = "StringLike"
stringNotLike = "StringNotLike"
ipAddress = "IpAddress"
notIPAddress = "NotIpAddress"
null = "Null"
)
// IsValid - checks if name is valid or not.
func (n name) IsValid() bool {
switch n {
case stringEquals, stringNotEquals, stringLike, stringNotLike, ipAddress, notIPAddress, null:
return true
}
return false
}
// MarshalJSON - encodes name to JSON data.
func (n name) MarshalJSON() ([]byte, error) {
if !n.IsValid() {
return nil, fmt.Errorf("invalid name %v", n)
}
return json.Marshal(string(n))
}
// UnmarshalJSON - decodes JSON data to condition name.
func (n *name) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
parsedName, err := parseName(s)
if err != nil {
return err
}
*n = parsedName
return nil
}
func parseName(s string) (name, error) {
n := name(s)
if n.IsValid() {
return n, nil
}
return n, fmt.Errorf("invalid condition name '%v'", s)
}

View File

@ -0,0 +1,106 @@
/*
* 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 condition
import (
"encoding/json"
"reflect"
"testing"
)
func TestNameIsValid(t *testing.T) {
testCases := []struct {
n name
expectedResult bool
}{
{stringEquals, true},
{stringNotEquals, true},
{stringLike, true},
{stringNotLike, true},
{ipAddress, true},
{notIPAddress, true},
{null, true},
{name("foo"), false},
}
for i, testCase := range testCases {
result := testCase.n.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 {
n name
expectedResult []byte
expectErr bool
}{
{stringEquals, []byte(`"StringEquals"`), false},
{stringNotEquals, []byte(`"StringNotEquals"`), false},
{stringLike, []byte(`"StringLike"`), false},
{stringNotLike, []byte(`"StringNotLike"`), false},
{ipAddress, []byte(`"IpAddress"`), false},
{notIPAddress, []byte(`"NotIpAddress"`), false},
{null, []byte(`"Null"`), false},
{name("foo"), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.n)
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 name
expectErr bool
}{
{[]byte(`"StringEquals"`), stringEquals, false},
{[]byte(`"foo"`), name(""), true},
}
for i, testCase := range testCases {
var result name
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)
}
}
}
}

View File

@ -0,0 +1,101 @@
/*
* 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 condition
import (
"fmt"
"reflect"
"strconv"
)
// nullFunc - Null condition function. It checks whether Key is present in given
// values or not.
// For example,
// 1. if Key = S3XAmzCopySource and Value = true, at evaluate() it returns whether
// S3XAmzCopySource is in given value map or not.
// 2. if Key = S3XAmzCopySource and Value = false, at evaluate() it returns whether
// S3XAmzCopySource is NOT in given value map or not.
type nullFunc struct {
k Key
value bool
}
// evaluate() - evaluates to check whether Key is present in given values or not.
// Depending on condition boolean value, this function returns true or false.
func (f nullFunc) evaluate(values map[string][]string) bool {
requestValue := values[f.k.Name()]
if f.value {
return len(requestValue) != 0
}
return len(requestValue) == 0
}
// key() - returns condition key which is used by this condition function.
func (f nullFunc) key() Key {
return f.k
}
// name() - returns "Null" condition name.
func (f nullFunc) name() name {
return null
}
func (f nullFunc) String() string {
return fmt.Sprintf("%v:%v:%v", null, f.k, f.value)
}
// toMap - returns map representation of this function.
func (f nullFunc) toMap() map[Key]ValueSet {
if !f.k.IsValid() {
return nil
}
return map[Key]ValueSet{
f.k: NewValueSet(NewBoolValue(f.value)),
}
}
func newNullFunc(key Key, values ValueSet) (Function, error) {
if len(values) != 1 {
return nil, fmt.Errorf("only one value is allowed for Null condition")
}
var value bool
for v := range values {
switch v.GetType() {
case reflect.Bool:
value, _ = v.GetBool()
case reflect.String:
var err error
s, _ := v.GetString()
if value, err = strconv.ParseBool(s); err != nil {
return nil, fmt.Errorf("value must be a boolean string for Null condition")
}
default:
return nil, fmt.Errorf("value must be a boolean for Null condition")
}
}
return &nullFunc{key, value}, nil
}
// NewNullFunc - returns new Null function.
func NewNullFunc(key Key, value bool) (Function, error) {
return &nullFunc{key, value}, nil
}

View File

@ -0,0 +1,161 @@
/*
* 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 condition
import (
"reflect"
"testing"
)
func TestNullFuncEvaluate(t *testing.T) {
case1Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(true)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(false)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
values map[string][]string
expectedResult bool
}{
{case1Function, map[string][]string{"prefix": {"true"}}, true},
{case1Function, map[string][]string{"prefix": {"false"}}, true},
{case1Function, map[string][]string{"prefix": {"mybucket/foo"}}, true},
{case1Function, map[string][]string{}, false},
{case1Function, map[string][]string{"delimiter": {"/"}}, false},
{case2Function, map[string][]string{"prefix": {"true"}}, false},
{case2Function, map[string][]string{"prefix": {"false"}}, false},
{case2Function, map[string][]string{"prefix": {"mybucket/foo"}}, false},
{case2Function, map[string][]string{}, true},
{case2Function, map[string][]string{"delimiter": {"/"}}, true},
}
for i, testCase := range testCases {
result := testCase.function.evaluate(testCase.values)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestNullFuncKey(t *testing.T) {
case1Function, err := newNullFunc(S3XAmzCopySource, NewValueSet(NewBoolValue(true)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
expectedResult Key
}{
{case1Function, S3XAmzCopySource},
}
for i, testCase := range testCases {
result := testCase.function.key()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestNullFuncToMap(t *testing.T) {
case1Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(true)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case1Result := map[Key]ValueSet{
S3Prefix: NewValueSet(NewBoolValue(true)),
}
case2Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(false)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Result := map[Key]ValueSet{
S3Prefix: NewValueSet(NewBoolValue(false)),
}
testCases := []struct {
f Function
expectedResult map[Key]ValueSet
}{
{case1Function, case1Result},
{case2Function, case2Result},
{&nullFunc{}, nil},
}
for i, testCase := range testCases {
result := testCase.f.toMap()
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestNewNullFunc(t *testing.T) {
case1Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(true)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(false)))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
key Key
values ValueSet
expectedResult Function
expectErr bool
}{
{S3Prefix, NewValueSet(NewBoolValue(true)), case1Function, false},
{S3Prefix, NewValueSet(NewStringValue("false")), case2Function, false},
// Multiple values error.
{S3Prefix, NewValueSet(NewBoolValue(true), NewBoolValue(false)), nil, true},
// Invalid boolean string error.
{S3Prefix, NewValueSet(NewStringValue("foo")), nil, true},
// Invalid value error.
{S3Prefix, NewValueSet(NewIntValue(7)), nil, true},
}
for i, testCase := range testCases {
result, err := newNullFunc(testCase.key, testCase.values)
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 !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}

View File

@ -0,0 +1,182 @@
/*
* 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 condition
import (
"fmt"
"sort"
"strings"
"github.com/minio/minio-go/pkg/set"
)
func toStringEqualsFuncString(n name, key Key, values set.StringSet) string {
valueStrings := values.ToSlice()
sort.Strings(valueStrings)
return fmt.Sprintf("%v:%v:%v", n, key, valueStrings)
}
// stringEqualsFunc - String equals function. It checks whether value by Key in given
// values map is in condition values.
// For example,
// - if values = ["mybucket/foo"], at evaluate() it returns whether string
// in value map for Key is in values.
type stringEqualsFunc struct {
k Key
values set.StringSet
}
// evaluate() - evaluates to check whether value by Key in given values is in
// condition values.
func (f stringEqualsFunc) evaluate(values map[string][]string) bool {
requestValue := values[f.k.Name()]
return !f.values.Intersection(set.CreateStringSet(requestValue...)).IsEmpty()
}
// key() - returns condition key which is used by this condition function.
func (f stringEqualsFunc) key() Key {
return f.k
}
// name() - returns "StringEquals" condition name.
func (f stringEqualsFunc) name() name {
return stringEquals
}
func (f stringEqualsFunc) String() string {
return toStringEqualsFuncString(stringEquals, f.k, f.values)
}
// toMap - returns map representation of this function.
func (f stringEqualsFunc) toMap() map[Key]ValueSet {
if !f.k.IsValid() {
return nil
}
values := NewValueSet()
for _, value := range f.values.ToSlice() {
values.Add(NewStringValue(value))
}
return map[Key]ValueSet{
f.k: values,
}
}
// stringNotEqualsFunc - String not equals function. It checks whether value by Key in
// given values is NOT in condition values.
// For example,
// - if values = ["mybucket/foo"], at evaluate() it returns whether string
// in value map for Key is NOT in values.
type stringNotEqualsFunc struct {
stringEqualsFunc
}
// evaluate() - evaluates to check whether value by Key in given values is NOT in
// condition values.
func (f stringNotEqualsFunc) evaluate(values map[string][]string) bool {
return !f.stringEqualsFunc.evaluate(values)
}
// name() - returns "StringNotEquals" condition name.
func (f stringNotEqualsFunc) name() name {
return stringNotEquals
}
func (f stringNotEqualsFunc) String() string {
return toStringEqualsFuncString(stringNotEquals, f.stringEqualsFunc.k, f.stringEqualsFunc.values)
}
func valuesToStringSlice(n name, values ValueSet) ([]string, error) {
valueStrings := []string{}
for value := range values {
// FIXME: if AWS supports non-string values, we would need to support it.
s, err := value.GetString()
if err != nil {
return nil, fmt.Errorf("value must be a string for %v condition", n)
}
valueStrings = append(valueStrings, s)
}
return valueStrings, nil
}
func validateStringEqualsValues(n name, key Key, values set.StringSet) error {
for _, s := range values.ToSlice() {
switch key {
case S3XAmzCopySource:
tokens := strings.SplitN(s, "/", 2)
if len(tokens) < 2 {
return fmt.Errorf("invalid value '%v' for '%v' for %v condition", s, S3XAmzCopySource, n)
}
// FIXME: tokens[0] must be a valid bucket name.
case S3XAmzServerSideEncryption:
if s != "aws:kms" && s != "AES256" {
return fmt.Errorf("invalid value '%v' for '%v' for %v condition", s, S3XAmzServerSideEncryption, n)
}
case S3XAmzMetadataDirective:
if s != "COPY" && s != "REPLACE" {
return fmt.Errorf("invalid value '%v' for '%v' for %v condition", s, S3XAmzMetadataDirective, n)
}
}
}
return nil
}
// newStringEqualsFunc - returns new StringEquals function.
func newStringEqualsFunc(key Key, values ValueSet) (Function, error) {
valueStrings, err := valuesToStringSlice(stringEquals, values)
if err != nil {
return nil, err
}
return NewStringEqualsFunc(key, valueStrings...)
}
// NewStringEqualsFunc - returns new StringEquals function.
func NewStringEqualsFunc(key Key, values ...string) (Function, error) {
sset := set.CreateStringSet(values...)
if err := validateStringEqualsValues(stringEquals, key, sset); err != nil {
return nil, err
}
return &stringEqualsFunc{key, sset}, nil
}
// newStringNotEqualsFunc - returns new StringNotEquals function.
func newStringNotEqualsFunc(key Key, values ValueSet) (Function, error) {
valueStrings, err := valuesToStringSlice(stringNotEquals, values)
if err != nil {
return nil, err
}
return NewStringNotEqualsFunc(key, valueStrings...)
}
// NewStringNotEqualsFunc - returns new StringNotEquals function.
func NewStringNotEqualsFunc(key Key, values ...string) (Function, error) {
sset := set.CreateStringSet(values...)
if err := validateStringEqualsValues(stringNotEquals, key, sset); err != nil {
return nil, err
}
return &stringNotEqualsFunc{stringEqualsFunc{key, sset}}, nil
}

View File

@ -0,0 +1,718 @@
/*
* 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 condition
import (
"reflect"
"testing"
)
func TestStringEqualsFuncEvaluate(t *testing.T) {
case1Function, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
values map[string][]string
expectedResult bool
}{
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, true},
{case1Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, false},
{case1Function, map[string][]string{}, false},
{case1Function, map[string][]string{"delimiter": {"/"}}, false},
{case2Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, true},
{case2Function, map[string][]string{"x-amz-server-side-encryption": {"aws:kms"}}, false},
{case2Function, map[string][]string{}, false},
{case2Function, map[string][]string{"delimiter": {"/"}}, false},
{case3Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, true},
{case3Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, false},
{case3Function, map[string][]string{}, false},
{case3Function, map[string][]string{"delimiter": {"/"}}, false},
{case4Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, true},
{case4Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, false},
{case4Function, map[string][]string{}, false},
{case4Function, map[string][]string{"delimiter": {"/"}}, false},
}
for i, testCase := range testCases {
result := testCase.function.evaluate(testCase.values)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestStringEqualsFuncKey(t *testing.T) {
case1Function, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
expectedResult Key
}{
{case1Function, S3XAmzCopySource},
{case2Function, S3XAmzServerSideEncryption},
{case3Function, S3XAmzMetadataDirective},
{case4Function, S3LocationConstraint},
}
for i, testCase := range testCases {
result := testCase.function.key()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestStringEqualsFuncToMap(t *testing.T) {
case1Function, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case1Result := map[Key]ValueSet{
S3XAmzCopySource: NewValueSet(NewStringValue("mybucket/myobject")),
}
case2Function, err := newStringEqualsFunc(S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/myobject"),
NewStringValue("yourbucket/myobject"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Result := map[Key]ValueSet{
S3XAmzCopySource: NewValueSet(
NewStringValue("mybucket/myobject"),
NewStringValue("yourbucket/myobject"),
),
}
case3Function, err := newStringEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Result := map[Key]ValueSet{
S3XAmzServerSideEncryption: NewValueSet(NewStringValue("AES256")),
}
case4Function, err := newStringEqualsFunc(S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES256"),
NewStringValue("aws:kms"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Result := map[Key]ValueSet{
S3XAmzServerSideEncryption: NewValueSet(
NewStringValue("AES256"),
NewStringValue("aws:kms"),
),
}
case5Function, err := newStringEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Result := map[Key]ValueSet{
S3XAmzMetadataDirective: NewValueSet(NewStringValue("REPLACE")),
}
case6Function, err := newStringEqualsFunc(S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPLACE"),
NewStringValue("COPY"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Result := map[Key]ValueSet{
S3XAmzMetadataDirective: NewValueSet(
NewStringValue("REPLACE"),
NewStringValue("COPY"),
),
}
case7Function, err := newStringEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Result := map[Key]ValueSet{
S3LocationConstraint: NewValueSet(NewStringValue("eu-west-1")),
}
case8Function, err := newStringEqualsFunc(S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-1"),
NewStringValue("us-west-1"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Result := map[Key]ValueSet{
S3LocationConstraint: NewValueSet(
NewStringValue("eu-west-1"),
NewStringValue("us-west-1"),
),
}
testCases := []struct {
f Function
expectedResult map[Key]ValueSet
}{
{case1Function, case1Result},
{case2Function, case2Result},
{case3Function, case3Result},
{case4Function, case4Result},
{case5Function, case5Result},
{case6Function, case6Result},
{case7Function, case7Result},
{case8Function, case8Result},
{&stringEqualsFunc{}, nil},
}
for i, testCase := range testCases {
result := testCase.f.toMap()
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestStringNotEqualsFuncEvaluate(t *testing.T) {
case1Function, err := newStringNotEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringNotEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringNotEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
values map[string][]string
expectedResult bool
}{
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, false},
{case1Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, true},
{case1Function, map[string][]string{}, true},
{case1Function, map[string][]string{"delimiter": {"/"}}, true},
{case2Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, false},
{case2Function, map[string][]string{"x-amz-server-side-encryption": {"aws:kms"}}, true},
{case2Function, map[string][]string{}, true},
{case2Function, map[string][]string{"delimiter": {"/"}}, true},
{case3Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, false},
{case3Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, true},
{case3Function, map[string][]string{}, true},
{case3Function, map[string][]string{"delimiter": {"/"}}, true},
{case4Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, false},
{case4Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, true},
{case4Function, map[string][]string{}, true},
{case4Function, map[string][]string{"delimiter": {"/"}}, true},
}
for i, testCase := range testCases {
result := testCase.function.evaluate(testCase.values)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestStringNotEqualsFuncKey(t *testing.T) {
case1Function, err := newStringNotEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringNotEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringNotEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
expectedResult Key
}{
{case1Function, S3XAmzCopySource},
{case2Function, S3XAmzServerSideEncryption},
{case3Function, S3XAmzMetadataDirective},
{case4Function, S3LocationConstraint},
}
for i, testCase := range testCases {
result := testCase.function.key()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestStringNotEqualsFuncToMap(t *testing.T) {
case1Function, err := newStringNotEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case1Result := map[Key]ValueSet{
S3XAmzCopySource: NewValueSet(NewStringValue("mybucket/myobject")),
}
case2Function, err := newStringNotEqualsFunc(S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/myobject"),
NewStringValue("yourbucket/myobject"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Result := map[Key]ValueSet{
S3XAmzCopySource: NewValueSet(
NewStringValue("mybucket/myobject"),
NewStringValue("yourbucket/myobject"),
),
}
case3Function, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Result := map[Key]ValueSet{
S3XAmzServerSideEncryption: NewValueSet(NewStringValue("AES256")),
}
case4Function, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES256"),
NewStringValue("aws:kms"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Result := map[Key]ValueSet{
S3XAmzServerSideEncryption: NewValueSet(
NewStringValue("AES256"),
NewStringValue("aws:kms"),
),
}
case5Function, err := newStringNotEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Result := map[Key]ValueSet{
S3XAmzMetadataDirective: NewValueSet(NewStringValue("REPLACE")),
}
case6Function, err := newStringNotEqualsFunc(S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPLACE"),
NewStringValue("COPY"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Result := map[Key]ValueSet{
S3XAmzMetadataDirective: NewValueSet(
NewStringValue("REPLACE"),
NewStringValue("COPY"),
),
}
case7Function, err := newStringNotEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Result := map[Key]ValueSet{
S3LocationConstraint: NewValueSet(NewStringValue("eu-west-1")),
}
case8Function, err := newStringNotEqualsFunc(S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-1"),
NewStringValue("us-west-1"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Result := map[Key]ValueSet{
S3LocationConstraint: NewValueSet(
NewStringValue("eu-west-1"),
NewStringValue("us-west-1"),
),
}
testCases := []struct {
f Function
expectedResult map[Key]ValueSet
}{
{case1Function, case1Result},
{case2Function, case2Result},
{case3Function, case3Result},
{case4Function, case4Result},
{case5Function, case5Result},
{case6Function, case6Result},
{case7Function, case7Result},
{case8Function, case8Result},
{&stringNotEqualsFunc{}, nil},
}
for i, testCase := range testCases {
result := testCase.f.toMap()
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestNewStringEqualsFunc(t *testing.T) {
case1Function, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringEqualsFunc(S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/myobject"),
NewStringValue("yourbucket/myobject"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringEqualsFunc(S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES256"),
NewStringValue("aws:kms"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Function, err := newStringEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Function, err := newStringEqualsFunc(S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPLACE"),
NewStringValue("COPY"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Function, err := newStringEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Function, err := newStringEqualsFunc(S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-1"),
NewStringValue("us-west-1"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
key Key
values ValueSet
expectedResult Function
expectErr bool
}{
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")), case1Function, false},
{S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/myobject"),
NewStringValue("yourbucket/myobject"),
), case2Function, false},
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")), case3Function, false},
{S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES256"),
NewStringValue("aws:kms"),
), case4Function, false},
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")), case5Function, false},
{S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPLACE"),
NewStringValue("COPY"),
), case6Function, false},
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")), case7Function, false},
{S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-1"),
NewStringValue("us-west-1"),
), case8Function, false},
// Unsupported value error.
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"), NewIntValue(7)), nil, true},
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"), NewIntValue(7)), nil, true},
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"), NewIntValue(7)), nil, true},
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"), NewIntValue(7)), nil, true},
// Invalid value error.
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket")), nil, true},
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("SSE-C")), nil, true},
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("DUPLICATE")), nil, true},
}
for i, testCase := range testCases {
result, err := newStringEqualsFunc(testCase.key, testCase.values)
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 !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}
func TestNewStringNotEqualsFunc(t *testing.T) {
case1Function, err := newStringNotEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringNotEqualsFunc(S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/myobject"),
NewStringValue("yourbucket/myobject"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES256"),
NewStringValue("aws:kms"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Function, err := newStringNotEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Function, err := newStringNotEqualsFunc(S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPLACE"),
NewStringValue("COPY"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Function, err := newStringNotEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Function, err := newStringNotEqualsFunc(S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-1"),
NewStringValue("us-west-1"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
key Key
values ValueSet
expectedResult Function
expectErr bool
}{
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")), case1Function, false},
{S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/myobject"),
NewStringValue("yourbucket/myobject"),
), case2Function, false},
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")), case3Function, false},
{S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES256"),
NewStringValue("aws:kms"),
), case4Function, false},
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")), case5Function, false},
{S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPLACE"),
NewStringValue("COPY"),
), case6Function, false},
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")), case7Function, false},
{S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-1"),
NewStringValue("us-west-1"),
), case8Function, false},
// Unsupported value error.
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"), NewIntValue(7)), nil, true},
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"), NewIntValue(7)), nil, true},
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"), NewIntValue(7)), nil, true},
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"), NewIntValue(7)), nil, true},
// Invalid value error.
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket")), nil, true},
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("SSE-C")), nil, true},
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("DUPLICATE")), nil, true},
}
for i, testCase := range testCases {
result, err := newStringNotEqualsFunc(testCase.key, testCase.values)
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 !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}

View File

@ -0,0 +1,165 @@
/*
* 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 condition
import (
"fmt"
"sort"
"strings"
"github.com/minio/minio-go/pkg/set"
"github.com/minio/minio/pkg/wildcard"
)
func toStringLikeFuncString(n name, key Key, values set.StringSet) string {
valueStrings := values.ToSlice()
sort.Strings(valueStrings)
return fmt.Sprintf("%v:%v:%v", n, key, valueStrings)
}
// stringLikeFunc - String like function. It checks whether value by Key in given
// values map is widcard 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 stringLikeFunc struct {
k Key
values set.StringSet
}
// evaluate() - evaluates to check whether value by Key in given values is wildcard
// matching in condition values.
func (f stringLikeFunc) evaluate(values map[string][]string) bool {
for _, v := range values[f.k.Name()] {
if !f.values.FuncMatch(wildcard.Match, v).IsEmpty() {
return true
}
}
return false
}
// key() - returns condition key which is used by this condition function.
func (f stringLikeFunc) key() Key {
return f.k
}
// name() - returns "StringLike" function name.
func (f stringLikeFunc) name() name {
return stringLike
}
func (f stringLikeFunc) String() string {
return toStringLikeFuncString(stringLike, f.k, f.values)
}
// toMap - returns map representation of this function.
func (f stringLikeFunc) toMap() map[Key]ValueSet {
if !f.k.IsValid() {
return nil
}
values := NewValueSet()
for _, value := range f.values.ToSlice() {
values.Add(NewStringValue(value))
}
return map[Key]ValueSet{
f.k: values,
}
}
// stringNotLikeFunc - String not like function. It checks whether value by Key in given
// values map is NOT widcard 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 stringNotLikeFunc struct {
stringLikeFunc
}
// evaluate() - evaluates to check whether value by Key in given values is NOT wildcard
// matching in condition values.
func (f stringNotLikeFunc) evaluate(values map[string][]string) bool {
return !f.stringLikeFunc.evaluate(values)
}
// name() - returns "StringNotLike" function name.
func (f stringNotLikeFunc) name() name {
return stringNotLike
}
func (f stringNotLikeFunc) String() string {
return toStringLikeFuncString(stringNotLike, f.stringLikeFunc.k, f.stringLikeFunc.values)
}
func validateStringLikeValues(n name, key Key, values set.StringSet) error {
for _, s := range values.ToSlice() {
switch key {
case S3XAmzCopySource:
tokens := strings.SplitN(s, "/", 2)
if len(tokens) < 2 {
return fmt.Errorf("invalid value '%v' for '%v' in %v condition", s, key, n)
}
// FIXME: tokens[0] must be a valid bucket name.
}
}
return nil
}
// newStringLikeFunc - returns new StringLike function.
func newStringLikeFunc(key Key, values ValueSet) (Function, error) {
valueStrings, err := valuesToStringSlice(stringLike, values)
if err != nil {
return nil, err
}
return NewStringLikeFunc(key, valueStrings...)
}
// NewStringLikeFunc - returns new StringLike function.
func NewStringLikeFunc(key Key, values ...string) (Function, error) {
sset := set.CreateStringSet(values...)
if err := validateStringLikeValues(stringLike, key, sset); err != nil {
return nil, err
}
return &stringLikeFunc{key, sset}, nil
}
// newStringNotLikeFunc - returns new StringNotLike function.
func newStringNotLikeFunc(key Key, values ValueSet) (Function, error) {
valueStrings, err := valuesToStringSlice(stringNotLike, values)
if err != nil {
return nil, err
}
return NewStringNotLikeFunc(key, valueStrings...)
}
// NewStringNotLikeFunc - returns new StringNotLike function.
func NewStringNotLikeFunc(key Key, values ...string) (Function, error) {
sset := set.CreateStringSet(values...)
if err := validateStringLikeValues(stringNotLike, key, sset); err != nil {
return nil, err
}
return &stringNotLikeFunc{stringLikeFunc{key, sset}}, nil
}

View File

@ -0,0 +1,810 @@
/*
* 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 condition
import (
"reflect"
"testing"
)
func TestStringLikeFuncEvaluate(t *testing.T) {
case1Function, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Function, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Function, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Function, err := newStringLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Function, err := newStringLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
values map[string][]string
expectedResult bool
}{
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, true},
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject.png"}}, true},
{case1Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, false},
{case1Function, map[string][]string{}, false},
{case1Function, map[string][]string{"delimiter": {"/"}}, false},
{case2Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, true},
{case2Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject.png"}}, false},
{case2Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, false},
{case2Function, map[string][]string{}, false},
{case2Function, map[string][]string{"delimiter": {"/"}}, false},
{case3Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, true},
{case3Function, map[string][]string{"x-amz-server-side-encryption": {"AES512"}}, true},
{case3Function, map[string][]string{"x-amz-server-side-encryption": {"aws:kms"}}, false},
{case3Function, map[string][]string{}, false},
{case3Function, map[string][]string{"delimiter": {"/"}}, false},
{case4Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, true},
{case4Function, map[string][]string{"x-amz-server-side-encryption": {"AES512"}}, false},
{case4Function, map[string][]string{"x-amz-server-side-encryption": {"aws:kms"}}, false},
{case4Function, map[string][]string{}, false},
{case4Function, map[string][]string{"delimiter": {"/"}}, false},
{case5Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, true},
{case5Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE/COPY"}}, true},
{case5Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, false},
{case5Function, map[string][]string{}, false},
{case5Function, map[string][]string{"delimiter": {"/"}}, false},
{case6Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, true},
{case6Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE/COPY"}}, false},
{case6Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, false},
{case6Function, map[string][]string{}, false},
{case6Function, map[string][]string{"delimiter": {"/"}}, false},
{case7Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, true},
{case7Function, map[string][]string{"LocationConstraint": {"eu-west-2"}}, true},
{case7Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, false},
{case7Function, map[string][]string{}, false},
{case7Function, map[string][]string{"delimiter": {"/"}}, false},
{case8Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, true},
{case8Function, map[string][]string{"LocationConstraint": {"eu-west-2"}}, false},
{case8Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, false},
{case8Function, map[string][]string{}, false},
{case8Function, map[string][]string{"delimiter": {"/"}}, false},
}
for i, testCase := range testCases {
result := testCase.function.evaluate(testCase.values)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestStringLikeFuncKey(t *testing.T) {
case1Function, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
expectedResult Key
}{
{case1Function, S3XAmzCopySource},
{case2Function, S3XAmzServerSideEncryption},
{case3Function, S3XAmzMetadataDirective},
{case4Function, S3LocationConstraint},
}
for i, testCase := range testCases {
result := testCase.function.key()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestStringLikeFuncToMap(t *testing.T) {
case1Function, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case1Result := map[Key]ValueSet{
S3XAmzCopySource: NewValueSet(NewStringValue("mybucket/*")),
}
case2Function, err := newStringLikeFunc(S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/*"),
NewStringValue("yourbucket/myobject*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Result := map[Key]ValueSet{
S3XAmzCopySource: NewValueSet(
NewStringValue("mybucket/*"),
NewStringValue("yourbucket/myobject*"),
),
}
case3Function, err := newStringLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Result := map[Key]ValueSet{
S3XAmzServerSideEncryption: NewValueSet(NewStringValue("AES*")),
}
case4Function, err := newStringLikeFunc(S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES*"),
NewStringValue("aws:*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Result := map[Key]ValueSet{
S3XAmzServerSideEncryption: NewValueSet(
NewStringValue("AES*"),
NewStringValue("aws:*"),
),
}
case5Function, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Result := map[Key]ValueSet{
S3XAmzMetadataDirective: NewValueSet(NewStringValue("REPL*")),
}
case6Function, err := newStringLikeFunc(S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPL*"),
NewStringValue("COPY*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Result := map[Key]ValueSet{
S3XAmzMetadataDirective: NewValueSet(
NewStringValue("REPL*"),
NewStringValue("COPY*"),
),
}
case7Function, err := newStringLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Result := map[Key]ValueSet{
S3LocationConstraint: NewValueSet(NewStringValue("eu-west-*")),
}
case8Function, err := newStringLikeFunc(S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-*"),
NewStringValue("us-west-*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Result := map[Key]ValueSet{
S3LocationConstraint: NewValueSet(
NewStringValue("eu-west-*"),
NewStringValue("us-west-*"),
),
}
testCases := []struct {
f Function
expectedResult map[Key]ValueSet
}{
{case1Function, case1Result},
{case2Function, case2Result},
{case3Function, case3Result},
{case4Function, case4Result},
{case5Function, case5Result},
{case6Function, case6Result},
{case7Function, case7Result},
{case8Function, case8Result},
{&stringLikeFunc{}, nil},
}
for i, testCase := range testCases {
result := testCase.f.toMap()
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestStringNotLikeFuncEvaluate(t *testing.T) {
case1Function, err := newStringNotLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringNotLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Function, err := newStringNotLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Function, err := newStringNotLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
values map[string][]string
expectedResult bool
}{
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, false},
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject.png"}}, false},
{case1Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, true},
{case1Function, map[string][]string{}, true},
{case1Function, map[string][]string{"delimiter": {"/"}}, true},
{case2Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, false},
{case2Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject.png"}}, true},
{case2Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, true},
{case2Function, map[string][]string{}, true},
{case2Function, map[string][]string{"delimiter": {"/"}}, true},
{case3Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, false},
{case3Function, map[string][]string{"x-amz-server-side-encryption": {"AES512"}}, false},
{case3Function, map[string][]string{"x-amz-server-side-encryption": {"aws:kms"}}, true},
{case3Function, map[string][]string{}, true},
{case3Function, map[string][]string{"delimiter": {"/"}}, true},
{case4Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, false},
{case4Function, map[string][]string{"x-amz-server-side-encryption": {"AES512"}}, true},
{case4Function, map[string][]string{"x-amz-server-side-encryption": {"aws:kms"}}, true},
{case4Function, map[string][]string{}, true},
{case4Function, map[string][]string{"delimiter": {"/"}}, true},
{case5Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, false},
{case5Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE/COPY"}}, false},
{case5Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, true},
{case5Function, map[string][]string{}, true},
{case5Function, map[string][]string{"delimiter": {"/"}}, true},
{case6Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, false},
{case6Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE/COPY"}}, true},
{case6Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, true},
{case6Function, map[string][]string{}, true},
{case6Function, map[string][]string{"delimiter": {"/"}}, true},
{case7Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, false},
{case7Function, map[string][]string{"LocationConstraint": {"eu-west-2"}}, false},
{case7Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, true},
{case7Function, map[string][]string{}, true},
{case7Function, map[string][]string{"delimiter": {"/"}}, true},
{case8Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, false},
{case8Function, map[string][]string{"LocationConstraint": {"eu-west-2"}}, true},
{case8Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, true},
{case8Function, map[string][]string{}, true},
{case8Function, map[string][]string{"delimiter": {"/"}}, true},
}
for i, testCase := range testCases {
result := testCase.function.evaluate(testCase.values)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestStringNotLikeFuncKey(t *testing.T) {
case1Function, err := newStringNotLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringNotLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
function Function
expectedResult Key
}{
{case1Function, S3XAmzCopySource},
{case2Function, S3XAmzServerSideEncryption},
{case3Function, S3XAmzMetadataDirective},
{case4Function, S3LocationConstraint},
}
for i, testCase := range testCases {
result := testCase.function.key()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestStringNotLikeFuncToMap(t *testing.T) {
case1Function, err := newStringNotLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case1Result := map[Key]ValueSet{
S3XAmzCopySource: NewValueSet(NewStringValue("mybucket/*")),
}
case2Function, err := newStringNotLikeFunc(S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/*"),
NewStringValue("yourbucket/myobject*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Result := map[Key]ValueSet{
S3XAmzCopySource: NewValueSet(
NewStringValue("mybucket/*"),
NewStringValue("yourbucket/myobject*"),
),
}
case3Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Result := map[Key]ValueSet{
S3XAmzServerSideEncryption: NewValueSet(NewStringValue("AES*")),
}
case4Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES*"),
NewStringValue("aws:*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Result := map[Key]ValueSet{
S3XAmzServerSideEncryption: NewValueSet(
NewStringValue("AES*"),
NewStringValue("aws:*"),
),
}
case5Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Result := map[Key]ValueSet{
S3XAmzMetadataDirective: NewValueSet(NewStringValue("REPL*")),
}
case6Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPL*"),
NewStringValue("COPY*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Result := map[Key]ValueSet{
S3XAmzMetadataDirective: NewValueSet(
NewStringValue("REPL*"),
NewStringValue("COPY*"),
),
}
case7Function, err := newStringNotLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Result := map[Key]ValueSet{
S3LocationConstraint: NewValueSet(NewStringValue("eu-west-*")),
}
case8Function, err := newStringNotLikeFunc(S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-*"),
NewStringValue("us-west-*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Result := map[Key]ValueSet{
S3LocationConstraint: NewValueSet(
NewStringValue("eu-west-*"),
NewStringValue("us-west-*"),
),
}
testCases := []struct {
f Function
expectedResult map[Key]ValueSet
}{
{case1Function, case1Result},
{case2Function, case2Result},
{case3Function, case3Result},
{case4Function, case4Result},
{case5Function, case5Result},
{case6Function, case6Result},
{case7Function, case7Result},
{case8Function, case8Result},
{&stringNotLikeFunc{}, nil},
}
for i, testCase := range testCases {
result := testCase.f.toMap()
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestNewStringLikeFunc(t *testing.T) {
case1Function, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringLikeFunc(S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/*"),
NewStringValue("yourbucket/myobject*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringLikeFunc(S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES*"),
NewStringValue("aws:*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Function, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Function, err := newStringLikeFunc(S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPL*"),
NewStringValue("COPY*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Function, err := newStringLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Function, err := newStringLikeFunc(S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-*"),
NewStringValue("us-west-*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
key Key
values ValueSet
expectedResult Function
expectErr bool
}{
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")), case1Function, false},
{S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/*"),
NewStringValue("yourbucket/myobject*"),
), case2Function, false},
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")), case3Function, false},
{S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES*"),
NewStringValue("aws:*"),
), case4Function, false},
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")), case5Function, false},
{S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPL*"),
NewStringValue("COPY*"),
), case6Function, false},
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")), case7Function, false},
{S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-*"),
NewStringValue("us-west-*"),
), case8Function, false},
// Unsupported value error.
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"), NewIntValue(7)), nil, true},
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"), NewIntValue(7)), nil, true},
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"), NewIntValue(7)), nil, true},
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"), NewIntValue(7)), nil, true},
// Invalid value error.
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket")), nil, true},
}
for i, testCase := range testCases {
result, err := newStringLikeFunc(testCase.key, testCase.values)
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 !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}
func TestNewStringNotLikeFunc(t *testing.T) {
case1Function, err := newStringNotLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Function, err := newStringNotLikeFunc(S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/*"),
NewStringValue("yourbucket/myobject*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case4Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES*"),
NewStringValue("aws:*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case5Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case6Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPL*"),
NewStringValue("COPY*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case7Function, err := newStringNotLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case8Function, err := newStringNotLikeFunc(S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-*"),
NewStringValue("us-west-*"),
),
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
key Key
values ValueSet
expectedResult Function
expectErr bool
}{
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")), case1Function, false},
{S3XAmzCopySource,
NewValueSet(
NewStringValue("mybucket/*"),
NewStringValue("yourbucket/myobject*"),
), case2Function, false},
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")), case3Function, false},
{S3XAmzServerSideEncryption,
NewValueSet(
NewStringValue("AES*"),
NewStringValue("aws:*"),
), case4Function, false},
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")), case5Function, false},
{S3XAmzMetadataDirective,
NewValueSet(
NewStringValue("REPL*"),
NewStringValue("COPY*"),
), case6Function, false},
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")), case7Function, false},
{S3LocationConstraint,
NewValueSet(
NewStringValue("eu-west-*"),
NewStringValue("us-west-*"),
), case8Function, false},
// Unsupported value error.
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"), NewIntValue(7)), nil, true},
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"), NewIntValue(7)), nil, true},
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"), NewIntValue(7)), nil, true},
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"), NewIntValue(7)), nil, true},
// Invalid value error.
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket")), nil, true},
}
for i, testCase := range testCases {
result, err := newStringNotLikeFunc(testCase.key, testCase.values)
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 !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}

View File

@ -0,0 +1,157 @@
/*
* 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 condition
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
)
// Value - is enum type of string, int or bool.
type Value struct {
t reflect.Kind
s string
i int
b bool
}
// GetBool - 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
}
// GetInt - 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
}
// GetString - 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
}
// GetType - gets enum type.
func (v Value) GetType() reflect.Kind {
return v.t
}
// MarshalJSON - 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)
}
// StoreBool - stores bool value.
func (v *Value) StoreBool(b bool) {
*v = Value{t: reflect.Bool, b: b}
}
// StoreInt - stores int value.
func (v *Value) StoreInt(i int) {
*v = Value{t: reflect.Int, i: i}
}
// StoreString - stores string value.
func (v *Value) StoreString(s string) {
*v = Value{t: reflect.String, s: s}
}
// String - 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 ""
}
// UnmarshalJSON - 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("unknown json data '%v'", data)
}
// NewBoolValue - returns new bool value.
func NewBoolValue(b bool) Value {
value := &Value{}
value.StoreBool(b)
return *value
}
// NewIntValue - returns new int value.
func NewIntValue(i int) Value {
value := &Value{}
value.StoreInt(i)
return *value
}
// NewStringValue - returns new string value.
func NewStringValue(s string) Value {
value := &Value{}
value.StoreString(s)
return *value
}

View File

@ -0,0 +1,260 @@
/*
* 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 condition
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 TestValueMarshalJSON(t *testing.T) {
testCases := []struct {
value Value
expectedResult []byte
expectErr bool
}{
{NewBoolValue(true), []byte("true"), false},
{NewIntValue(7), []byte("7"), false},
{NewStringValue("foo"), []byte(`"foo"`), false},
{Value{}, nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.value)
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 !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}
func TestValueStoreBool(t *testing.T) {
testCases := []struct {
value bool
expectedResult Value
}{
{false, NewBoolValue(false)},
{true, NewBoolValue(true)},
}
for i, testCase := range testCases {
var result Value
result.StoreBool(testCase.value)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestValueStoreInt(t *testing.T) {
testCases := []struct {
value int
expectedResult Value
}{
{0, NewIntValue(0)},
{7, NewIntValue(7)},
}
for i, testCase := range testCases {
var result Value
result.StoreInt(testCase.value)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestValueStoreString(t *testing.T) {
testCases := []struct {
value string
expectedResult Value
}{
{"", NewStringValue("")},
{"foo", NewStringValue("foo")},
}
for i, testCase := range testCases {
var result Value
result.StoreString(testCase.value)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestValueString(t *testing.T) {
testCases := []struct {
value Value
expectedResult string
}{
{NewBoolValue(true), "true"},
{NewIntValue(7), "7"},
{NewStringValue("foo"), "foo"},
{Value{}, ""},
}
for i, testCase := range testCases {
result := testCase.value.String()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestValueUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult Value
expectErr bool
}{
{[]byte("true"), NewBoolValue(true), false},
{[]byte("7"), NewIntValue(7), false},
{[]byte(`"foo"`), NewStringValue("foo"), false},
{[]byte("True"), Value{}, true},
{[]byte("7.1"), Value{}, true},
{[]byte(`["foo"]`), Value{}, true},
}
for i, testCase := range testCases {
var result Value
err := json.Unmarshal(testCase.data, &result)
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 !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}

View File

@ -0,0 +1,85 @@
/*
* 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 condition
import (
"encoding/json"
"fmt"
)
// ValueSet - unique list of values.
type ValueSet map[Value]struct{}
// Add - adds given value to value set.
func (set ValueSet) Add(value Value) {
set[value] = struct{}{}
}
// MarshalJSON - encodes ValueSet to JSON data.
func (set ValueSet) MarshalJSON() ([]byte, error) {
var 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)
}
// UnmarshalJSON - 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("invalid value")
}
*set = make(ValueSet)
for _, v = range values {
if _, found := (*set)[v]; found {
return fmt.Errorf("duplicate value found '%v'", v)
}
set.Add(v)
}
return nil
}
// NewValueSet - 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,118 @@
/*
* 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 condition
import (
"encoding/json"
"reflect"
"testing"
)
func TestValueSetAdd(t *testing.T) {
testCases := []struct {
value Value
expectedResult ValueSet
}{
{NewBoolValue(true), NewValueSet(NewBoolValue(true))},
{NewIntValue(7), NewValueSet(NewIntValue(7))},
{NewStringValue("foo"), NewValueSet(NewStringValue("foo"))},
}
for i, testCase := range testCases {
result := NewValueSet()
result.Add(testCase.value)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestValueSetMarshalJSON(t *testing.T) {
testCases := []struct {
set ValueSet
expectedResult string
expectErr bool
}{
{NewValueSet(NewBoolValue(true)), `[true]`, false},
{NewValueSet(NewIntValue(7)), `[7]`, false},
{NewValueSet(NewStringValue("foo")), `["foo"]`, false},
{NewValueSet(NewBoolValue(true)), `[true]`, false},
{NewValueSet(NewStringValue("7")), `["7"]`, false},
{NewValueSet(NewStringValue("foo")), `["foo"]`, false},
{make(ValueSet), "", true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.set)
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 string(result) != testCase.expectedResult {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, string(result))
}
}
}
}
func TestValueSetUnmarshalJSON(t *testing.T) {
set1 := NewValueSet(
NewBoolValue(true),
NewStringValue("false"),
NewIntValue(7),
NewStringValue("7"),
NewStringValue("foo"),
NewStringValue("192.168.1.100/24"),
)
testCases := []struct {
data []byte
expectedResult ValueSet
expectErr bool
}{
{[]byte(`true`), NewValueSet(NewBoolValue(true)), false},
{[]byte(`7`), NewValueSet(NewIntValue(7)), false},
{[]byte(`"foo"`), NewValueSet(NewStringValue("foo")), false},
{[]byte(`[true]`), NewValueSet(NewBoolValue(true)), false},
{[]byte(`[7]`), NewValueSet(NewIntValue(7)), false},
{[]byte(`["foo"]`), NewValueSet(NewStringValue("foo")), false},
{[]byte(`[true, "false", 7, "7", "foo", "192.168.1.100/24"]`), set1, false},
{[]byte(`{}`), nil, true}, // Unsupported data.
{[]byte(`[]`), nil, true}, // Empty array.
{[]byte(`[7, 7, true]`), nil, true}, // Duplicate value.
}
for i, testCase := range testCases {
result := make(ValueSet)
err := json.Unmarshal(testCase.data, &result)
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 !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}

78
pkg/policy/effect.go Normal file
View File

@ -0,0 +1,78 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
)
// Effect - policy statement effect Allow or Deny.
type Effect string
const (
// Allow - allow effect.
Allow Effect = "Allow"
// Deny - deny effect.
Deny = "Deny"
)
// IsAllowed - returns if given check is allowed or not.
func (effect Effect) IsAllowed(b bool) bool {
if effect == Allow {
return b
}
return !b
}
// IsValid - checks if Effect is valid or not
func (effect Effect) IsValid() bool {
switch effect {
case Allow, Deny:
return true
}
return false
}
// MarshalJSON - encodes Effect to JSON data.
func (effect Effect) MarshalJSON() ([]byte, error) {
if !effect.IsValid() {
return nil, fmt.Errorf("invalid effect '%v'", effect)
}
return json.Marshal(string(effect))
}
// UnmarshalJSON - decodes JSON data to Effect.
func (effect *Effect) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
e := Effect(s)
if !e.IsValid() {
return fmt.Errorf("invalid effect '%v'", s)
}
*effect = e
return nil
}

122
pkg/policy/effect_test.go Normal file
View File

@ -0,0 +1,122 @@
/*
* 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 policy
import (
"encoding/json"
"reflect"
"testing"
)
func TestEffectIsAllowed(t *testing.T) {
testCases := []struct {
effect Effect
check bool
expectedResult bool
}{
{Allow, false, false},
{Allow, true, true},
{Deny, false, true},
{Deny, true, false},
}
for i, testCase := range testCases {
result := testCase.effect.IsAllowed(testCase.check)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestEffectIsValid(t *testing.T) {
testCases := []struct {
effect Effect
expectedResult bool
}{
{Allow, true},
{Deny, true},
{Effect(""), false},
{Effect("foo"), false},
}
for i, testCase := range testCases {
result := testCase.effect.IsValid()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestEffectMarshalJSON(t *testing.T) {
testCases := []struct {
effect Effect
expectedResult []byte
expectErr bool
}{
{Allow, []byte(`"Allow"`), false},
{Deny, []byte(`"Deny"`), false},
{Effect(""), nil, true},
{Effect("foo"), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.effect)
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 !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, string(testCase.expectedResult), string(result))
}
}
}
}
func TestEffectUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult Effect
expectErr bool
}{
{[]byte(`"Allow"`), Allow, false},
{[]byte(`"Deny"`), Deny, false},
{[]byte(`""`), Effect(""), true},
{[]byte(`"foo"`), Effect(""), true},
}
for i, testCase := range testCases {
var result Effect
err := json.Unmarshal(testCase.data, &result)
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)
}
}
}
}

64
pkg/policy/id.go Normal file
View File

@ -0,0 +1,64 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
"regexp"
)
var idRegexp = regexp.MustCompile("^[[:alnum:]]+$")
// ID - policy ID.
type ID string
// IsValid - checks if ID is valid or not.
func (id ID) IsValid() bool {
// Allow empty string as ID.
if string(id) == "" {
return true
}
return idRegexp.MatchString(string(id))
}
// MarshalJSON - encodes ID to JSON data.
func (id ID) MarshalJSON() ([]byte, error) {
if !id.IsValid() {
return nil, fmt.Errorf("invalid ID %v", id)
}
return json.Marshal(string(id))
}
// UnmarshalJSON - decodes JSON data to ID.
func (id *ID) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
i := ID(s)
if !i.IsValid() {
return fmt.Errorf("invalid ID %v", s)
}
*id = i
return nil
}

99
pkg/policy/id_test.go Normal file
View File

@ -0,0 +1,99 @@
/*
* 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 policy
import (
"encoding/json"
"reflect"
"testing"
)
func TestIDIsValid(t *testing.T) {
testCases := []struct {
id ID
expectedResult bool
}{
{ID("DenyEncryptionSt1"), true},
{ID(""), true},
}
for i, testCase := range testCases {
result := testCase.id.IsValid()
if result != testCase.expectedResult {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestIDMarshalJSON(t *testing.T) {
testCases := []struct {
id ID
expectedResult []byte
expectErr bool
}{
{ID("foo"), []byte(`"foo"`), false},
{ID("1234"), []byte(`"1234"`), false},
{ID("DenyEncryptionSt1"), []byte(`"DenyEncryptionSt1"`), false},
{ID(""), []byte(`""`), false},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.id)
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 !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, string(testCase.expectedResult), string(result))
}
}
}
}
func TestIDUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult ID
expectErr bool
}{
{[]byte(`"foo"`), ID("foo"), false},
{[]byte(`"1234"`), ID("1234"), false},
{[]byte(`"DenyEncryptionSt1"`), ID("DenyEncryptionSt1"), false},
{[]byte(`""`), ID(""), false},
{[]byte(`"foo bar"`), ID(""), true},
}
for i, testCase := range testCases {
var result ID
err := json.Unmarshal(testCase.data, &result)
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)
}
}
}
}

176
pkg/policy/policy.go Normal file
View File

@ -0,0 +1,176 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
"io"
)
// DefaultVersion - default policy version as per AWS S3 specification.
const DefaultVersion = "2012-10-17"
// Args - arguments to policy to check whether it is allowed
type Args struct {
AccountName string
Action Action
BucketName string
ConditionValues map[string][]string
IsOwner bool
ObjectName string
}
// Policy - bucket policy.
type Policy struct {
ID ID `json:"ID,omitempty"`
Version string
Statements []Statement `json:"Statement"`
}
// IsAllowed - checks given policy args is allowed to continue the Rest API.
func (policy Policy) IsAllowed(args Args) bool {
// Check all deny statements. If any one statement denies, return false.
for _, statement := range policy.Statements {
if statement.Effect == Deny {
if !statement.IsAllowed(args) {
return false
}
}
}
// For owner, its allowed by default.
if args.IsOwner {
return true
}
// Check all allow statements. If any one statement allows, return true.
for _, statement := range policy.Statements {
if statement.Effect == Allow {
if statement.IsAllowed(args) {
return true
}
}
}
return false
}
// IsEmpty - returns whether policy is empty or not.
func (policy Policy) IsEmpty() bool {
return len(policy.Statements) == 0
}
// isValid - checks if Policy is valid or not.
func (policy Policy) isValid() error {
if policy.Version != DefaultVersion {
return fmt.Errorf("invalid version '%v'", policy.Version)
}
for _, statement := range policy.Statements {
if err := statement.isValid(); err != nil {
return err
}
}
for i := range policy.Statements {
for _, statement := range policy.Statements[i+1:] {
principals := policy.Statements[i].Principal.Intersection(statement.Principal)
if principals.IsEmpty() {
continue
}
actions := policy.Statements[i].Actions.Intersection(statement.Actions)
if len(actions) == 0 {
continue
}
resources := policy.Statements[i].Resources.Intersection(statement.Resources)
if len(resources) == 0 {
continue
}
if policy.Statements[i].Conditions.String() != statement.Conditions.String() {
continue
}
return fmt.Errorf("duplicate principal %v, actions %v, resouces %v found in statements %v, %v",
principals, actions, resources, policy.Statements[i], statement)
}
}
return nil
}
// MarshalJSON - encodes Policy to JSON data.
func (policy Policy) MarshalJSON() ([]byte, error) {
if err := policy.isValid(); err != nil {
return nil, err
}
// subtype to avoid recursive call to MarshalJSON()
type subPolicy Policy
return json.Marshal(subPolicy(policy))
}
// UnmarshalJSON - decodes JSON data to Policy.
func (policy *Policy) UnmarshalJSON(data []byte) error {
// subtype to avoid recursive call to UnmarshalJSON()
type subPolicy Policy
var sp subPolicy
if err := json.Unmarshal(data, &sp); err != nil {
return err
}
p := Policy(sp)
if err := p.isValid(); err != nil {
return err
}
*policy = p
return nil
}
// Validate - validates all statements are for given bucket or not.
func (policy Policy) Validate(bucketName string) error {
if err := policy.isValid(); err != nil {
return err
}
for _, statement := range policy.Statements {
if err := statement.Validate(bucketName); err != nil {
return err
}
}
return nil
}
// ParseConfig - parses data in given reader to Policy.
func ParseConfig(reader io.Reader, bucketName string) (*Policy, error) {
var policy Policy
decoder := json.NewDecoder(reader)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&policy); err != nil {
return nil, err
}
err := policy.Validate(bucketName)
return &policy, err
}

1089
pkg/policy/policy_test.go Normal file

File diff suppressed because it is too large Load Diff

92
pkg/policy/principal.go Normal file
View File

@ -0,0 +1,92 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
"github.com/minio/minio-go/pkg/set"
"github.com/minio/minio/pkg/wildcard"
)
// Principal - policy principal.
type Principal struct {
AWS set.StringSet
}
// IsValid - checks whether Principal is valid or not.
func (p Principal) IsValid() bool {
return len(p.AWS) != 0
}
// Intersection - returns principals available in both Principal.
func (p Principal) Intersection(principal Principal) set.StringSet {
return p.AWS.Intersection(principal.AWS)
}
// MarshalJSON - encodes Principal to JSON data.
func (p Principal) MarshalJSON() ([]byte, error) {
if !p.IsValid() {
return nil, fmt.Errorf("invalid principal %v", p)
}
// subtype to avoid recursive call to MarshalJSON()
type subPrincipal Principal
sp := subPrincipal(p)
return json.Marshal(sp)
}
// Match - matches given principal is wildcard matching with Principal.
func (p Principal) Match(principal string) bool {
for _, pattern := range p.AWS.ToSlice() {
if wildcard.MatchSimple(pattern, principal) {
return true
}
}
return false
}
// UnmarshalJSON - decodes JSON data to Principal.
func (p *Principal) UnmarshalJSON(data []byte) error {
// subtype to avoid recursive call to UnmarshalJSON()
type subPrincipal Principal
var sp subPrincipal
if err := json.Unmarshal(data, &sp); err != nil {
var s string
if err = json.Unmarshal(data, &s); err != nil {
return err
}
if s != "*" {
return fmt.Errorf("invalid principal '%v'", s)
}
sp.AWS = set.CreateStringSet("*")
}
*p = Principal(sp)
return nil
}
// NewPrincipal - creates new Principal.
func NewPrincipal(principals ...string) Principal {
return Principal{AWS: set.CreateStringSet(principals...)}
}

View File

@ -0,0 +1,141 @@
/*
* 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 policy
import (
"encoding/json"
"reflect"
"testing"
"github.com/minio/minio-go/pkg/set"
)
func TestPrincipalIsValid(t *testing.T) {
testCases := []struct {
principal Principal
expectedResult bool
}{
{NewPrincipal("*"), true},
{NewPrincipal("arn:aws:iam::AccountNumber:root"), true},
{NewPrincipal(), false},
}
for i, testCase := range testCases {
result := testCase.principal.IsValid()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestPrincipalIntersection(t *testing.T) {
testCases := []struct {
principal Principal
principalToIntersect Principal
expectedResult set.StringSet
}{
{NewPrincipal("*"), NewPrincipal("*"), set.CreateStringSet("*")},
{NewPrincipal("arn:aws:iam::AccountNumber:root"), NewPrincipal("arn:aws:iam::AccountNumber:myuser"), set.CreateStringSet()},
{NewPrincipal(), NewPrincipal("*"), set.CreateStringSet()},
}
for i, testCase := range testCases {
result := testCase.principal.Intersection(testCase.principalToIntersect)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestPrincipalMarshalJSON(t *testing.T) {
testCases := []struct {
principal Principal
expectedResult []byte
expectErr bool
}{
{NewPrincipal("*"), []byte(`{"AWS":["*"]}`), false},
{NewPrincipal("arn:aws:iam::AccountNumber:*"), []byte(`{"AWS":["arn:aws:iam::AccountNumber:*"]}`), false},
{NewPrincipal(), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.principal)
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 !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, string(testCase.expectedResult), string(result))
}
}
}
}
func TestPrincipalMatch(t *testing.T) {
testCases := []struct {
principals Principal
principal string
expectedResult bool
}{
{NewPrincipal("*"), "AccountNumber", true},
{NewPrincipal("arn:aws:iam::*"), "arn:aws:iam::AccountNumber:root", true},
{NewPrincipal("arn:aws:iam::AccountNumber:*"), "arn:aws:iam::TestAccountNumber:root", false},
}
for i, testCase := range testCases {
result := testCase.principals.Match(testCase.principal)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestPrincipalUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult Principal
expectErr bool
}{
{[]byte(`"*"`), NewPrincipal("*"), false},
{[]byte(`{"AWS": "*"}`), NewPrincipal("*"), false},
{[]byte(`{"AWS": "arn:aws:iam::AccountNumber:*"}`), NewPrincipal("arn:aws:iam::AccountNumber:*"), false},
{[]byte(`"arn:aws:iam::AccountNumber:*"`), NewPrincipal(), true},
{[]byte(`["arn:aws:iam::AccountNumber:*", "arn:aws:iam:AnotherAccount:*"]`), NewPrincipal(), true},
}
for i, testCase := range testCases {
var result Principal
err := json.Unmarshal(testCase.data, &result)
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 !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
}

131
pkg/policy/resource.go Normal file
View File

@ -0,0 +1,131 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
"strings"
"github.com/minio/minio/pkg/wildcard"
)
// ResourceARNPrefix - resource ARN prefix as per AWS S3 specification.
const ResourceARNPrefix = "arn:aws:s3:::"
// Resource - resource in policy statement.
type Resource struct {
bucketName string
pattern string
}
func (r Resource) isBucketPattern() bool {
return !strings.Contains(r.pattern, "/")
}
func (r Resource) isObjectPattern() bool {
return strings.Contains(r.pattern, "/") || strings.Contains(r.bucketName, "*")
}
// IsValid - checks whether Resource is valid or not.
func (r Resource) IsValid() bool {
return r.bucketName != "" && r.pattern != ""
}
// Match - matches object name with resource pattern.
func (r Resource) Match(resource string) bool {
return wildcard.Match(r.pattern, resource)
}
// MarshalJSON - encodes Resource to JSON data.
func (r Resource) MarshalJSON() ([]byte, error) {
if !r.IsValid() {
return nil, fmt.Errorf("invalid resource %v", r)
}
return json.Marshal(r.String())
}
func (r Resource) String() string {
return ResourceARNPrefix + r.pattern
}
// UnmarshalJSON - decodes JSON data to Resource.
func (r *Resource) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
parsedResource, err := parseResource(s)
if err != nil {
return err
}
*r = parsedResource
return nil
}
// Validate - validates Resource is for given bucket or not.
func (r Resource) Validate(bucketName string) error {
if !r.IsValid() {
return fmt.Errorf("invalid resource")
}
if !wildcard.Match(r.bucketName, bucketName) {
return fmt.Errorf("bucket name does not match")
}
return nil
}
// parseResource - parses string to Resource.
func parseResource(s string) (Resource, error) {
if !strings.HasPrefix(s, ResourceARNPrefix) {
return Resource{}, fmt.Errorf("invalid resource '%v'", s)
}
pattern := strings.TrimPrefix(s, ResourceARNPrefix)
tokens := strings.SplitN(pattern, "/", 2)
bucketName := tokens[0]
if bucketName == "" {
return Resource{}, fmt.Errorf("invalid resource format '%v'", s)
}
return Resource{
bucketName: bucketName,
pattern: pattern,
}, nil
}
// NewResource - creates new resource.
func NewResource(bucketName, keyName string) Resource {
pattern := bucketName
if keyName != "" {
if !strings.HasPrefix(keyName, "/") {
pattern += "/"
}
pattern += keyName
}
return Resource{
bucketName: bucketName,
pattern: pattern,
}
}

221
pkg/policy/resource_test.go Normal file
View File

@ -0,0 +1,221 @@
/*
* 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 policy
import (
"encoding/json"
"reflect"
"testing"
)
func TestResourceIsBucketPattern(t *testing.T) {
testCases := []struct {
resource Resource
expectedResult bool
}{
{NewResource("*", ""), true},
{NewResource("mybucket", ""), true},
{NewResource("mybucket*", ""), true},
{NewResource("mybucket?0", ""), true},
{NewResource("", "*"), false},
{NewResource("*", "*"), false},
{NewResource("mybucket", "*"), false},
{NewResource("mybucket*", "/myobject"), false},
{NewResource("mybucket?0", "/2010/photos/*"), false},
}
for i, testCase := range testCases {
result := testCase.resource.isBucketPattern()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceIsObjectPattern(t *testing.T) {
testCases := []struct {
resource Resource
expectedResult bool
}{
{NewResource("*", ""), true},
{NewResource("mybucket*", ""), true},
{NewResource("", "*"), true},
{NewResource("*", "*"), true},
{NewResource("mybucket", "*"), true},
{NewResource("mybucket*", "/myobject"), true},
{NewResource("mybucket?0", "/2010/photos/*"), true},
{NewResource("mybucket", ""), false},
{NewResource("mybucket?0", ""), false},
}
for i, testCase := range testCases {
result := testCase.resource.isObjectPattern()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceIsValid(t *testing.T) {
testCases := []struct {
resource Resource
expectedResult bool
}{
{NewResource("*", ""), true},
{NewResource("mybucket*", ""), true},
{NewResource("*", "*"), true},
{NewResource("mybucket", "*"), true},
{NewResource("mybucket*", "/myobject"), true},
{NewResource("mybucket?0", "/2010/photos/*"), true},
{NewResource("mybucket", ""), true},
{NewResource("mybucket?0", ""), true},
{NewResource("", ""), false},
{NewResource("", "*"), false},
}
for i, testCase := range testCases {
result := testCase.resource.IsValid()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceMatch(t *testing.T) {
testCases := []struct {
resource Resource
objectName string
expectedResult bool
}{
{NewResource("*", ""), "mybucket", true},
{NewResource("*", ""), "mybucket/myobject", true},
{NewResource("mybucket*", ""), "mybucket", true},
{NewResource("mybucket*", ""), "mybucket/myobject", true},
{NewResource("", "*"), "/myobject", true},
{NewResource("*", "*"), "mybucket/myobject", true},
{NewResource("mybucket", "*"), "mybucket/myobject", true},
{NewResource("mybucket*", "/myobject"), "mybucket/myobject", true},
{NewResource("mybucket*", "/myobject"), "mybucket100/myobject", true},
{NewResource("mybucket?0", "/2010/photos/*"), "mybucket20/2010/photos/1.jpg", true},
{NewResource("mybucket", ""), "mybucket", true},
{NewResource("mybucket?0", ""), "mybucket30", true},
{NewResource("", "*"), "mybucket/myobject", false},
{NewResource("*", "*"), "mybucket", false},
{NewResource("mybucket", "*"), "mybucket10/myobject", false},
{NewResource("mybucket?0", "/2010/photos/*"), "mybucket0/2010/photos/1.jpg", false},
{NewResource("mybucket", ""), "mybucket/myobject", false},
}
for i, testCase := range testCases {
result := testCase.resource.Match(testCase.objectName)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceMarshalJSON(t *testing.T) {
testCases := []struct {
resource Resource
expectedResult []byte
expectErr bool
}{
{NewResource("*", ""), []byte(`"arn:aws:s3:::*"`), false},
{NewResource("mybucket*", ""), []byte(`"arn:aws:s3:::mybucket*"`), false},
{NewResource("mybucket", ""), []byte(`"arn:aws:s3:::mybucket"`), false},
{NewResource("*", "*"), []byte(`"arn:aws:s3:::*/*"`), false},
{NewResource("mybucket", "*"), []byte(`"arn:aws:s3:::mybucket/*"`), false},
{NewResource("mybucket*", "myobject"), []byte(`"arn:aws:s3:::mybucket*/myobject"`), false},
{NewResource("mybucket?0", "/2010/photos/*"), []byte(`"arn:aws:s3:::mybucket?0/2010/photos/*"`), false},
{Resource{}, nil, true},
{NewResource("", "*"), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.resource)
expectErr := (err != nil)
if expectErr != testCase.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 TestResourceUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult Resource
expectErr bool
}{
{[]byte(`"arn:aws:s3:::*"`), NewResource("*", ""), false},
{[]byte(`"arn:aws:s3:::mybucket*"`), NewResource("mybucket*", ""), false},
{[]byte(`"arn:aws:s3:::mybucket"`), NewResource("mybucket", ""), false},
{[]byte(`"arn:aws:s3:::*/*"`), NewResource("*", "*"), false},
{[]byte(`"arn:aws:s3:::mybucket/*"`), NewResource("mybucket", "*"), false},
{[]byte(`"arn:aws:s3:::mybucket*/myobject"`), NewResource("mybucket*", "myobject"), false},
{[]byte(`"arn:aws:s3:::mybucket?0/2010/photos/*"`), NewResource("mybucket?0", "/2010/photos/*"), false},
{[]byte(`"mybucket/myobject*"`), Resource{}, true},
{[]byte(`"arn:aws:s3:::/*"`), Resource{}, true},
}
for i, testCase := range testCases {
var result Resource
err := json.Unmarshal(testCase.data, &result)
expectErr := (err != nil)
if expectErr != testCase.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, testCase.expectedResult, result)
}
}
}
}
func TestResourceValidate(t *testing.T) {
testCases := []struct {
resource Resource
bucketName string
expectErr bool
}{
{NewResource("mybucket", "/myobject*"), "mybucket", false},
{NewResource("", "/myobject*"), "yourbucket", true},
{NewResource("mybucket", "/myobject*"), "yourbucket", true},
}
for i, testCase := range testCases {
err := testCase.resource.Validate(testCase.bucketName)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
}
}

147
pkg/policy/resourceset.go Normal file
View File

@ -0,0 +1,147 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
"sort"
"github.com/minio/minio-go/pkg/set"
)
// ResourceSet - set of resources in policy statement.
type ResourceSet map[Resource]struct{}
// bucketResourceExists - checks if at least one bucket resource exists in the set.
func (resourceSet ResourceSet) bucketResourceExists() bool {
for resource := range resourceSet {
if resource.isBucketPattern() {
return true
}
}
return false
}
// objectResourceExists - checks if at least one object resource exists in the set.
func (resourceSet ResourceSet) objectResourceExists() bool {
for resource := range resourceSet {
if resource.isObjectPattern() {
return true
}
}
return false
}
// Add - adds resource to resource set.
func (resourceSet ResourceSet) Add(resource Resource) {
resourceSet[resource] = struct{}{}
}
// Intersection - returns resouces available in both ResourcsSet.
func (resourceSet ResourceSet) Intersection(sset ResourceSet) ResourceSet {
nset := NewResourceSet()
for k := range resourceSet {
if _, ok := sset[k]; ok {
nset.Add(k)
}
}
return nset
}
// MarshalJSON - encodes ResourceSet to JSON data.
func (resourceSet ResourceSet) MarshalJSON() ([]byte, error) {
if len(resourceSet) == 0 {
return nil, fmt.Errorf("empty resource set")
}
resources := []Resource{}
for resource := range resourceSet {
resources = append(resources, resource)
}
return json.Marshal(resources)
}
// Match - matches object name with anyone of resource pattern in resource set.
func (resourceSet ResourceSet) Match(resource string) bool {
for r := range resourceSet {
if r.Match(resource) {
return true
}
}
return false
}
func (resourceSet ResourceSet) String() string {
resources := []string{}
for resource := range resourceSet {
resources = append(resources, resource.String())
}
sort.Strings(resources)
return fmt.Sprintf("%v", resources)
}
// UnmarshalJSON - decodes JSON data to ResourceSet.
func (resourceSet *ResourceSet) UnmarshalJSON(data []byte) error {
var sset set.StringSet
if err := json.Unmarshal(data, &sset); err != nil {
return err
}
*resourceSet = make(ResourceSet)
for _, s := range sset.ToSlice() {
resource, err := parseResource(s)
if err != nil {
return err
}
if _, found := (*resourceSet)[resource]; found {
return fmt.Errorf("duplicate resource '%v' found", s)
}
resourceSet.Add(resource)
}
return nil
}
// Validate - validates ResourceSet is for given bucket or not.
func (resourceSet ResourceSet) Validate(bucketName string) error {
for resource := range resourceSet {
if err := resource.Validate(bucketName); err != nil {
return err
}
}
return nil
}
// NewResourceSet - creates new resource set.
func NewResourceSet(resources ...Resource) ResourceSet {
resourceSet := make(ResourceSet)
for _, resource := range resources {
resourceSet.Add(resource)
}
return resourceSet
}

View File

@ -0,0 +1,240 @@
/*
* 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 policy
import (
"encoding/json"
"reflect"
"testing"
)
func TestResourceSetBucketResourceExists(t *testing.T) {
testCases := []struct {
resourceSet ResourceSet
expectedResult bool
}{
{NewResourceSet(NewResource("*", "")), true},
{NewResourceSet(NewResource("mybucket", "")), true},
{NewResourceSet(NewResource("mybucket*", "")), true},
{NewResourceSet(NewResource("mybucket?0", "")), true},
{NewResourceSet(NewResource("mybucket", "/2010/photos/*"), NewResource("mybucket", "")), true},
{NewResourceSet(NewResource("", "*")), false},
{NewResourceSet(NewResource("*", "*")), false},
{NewResourceSet(NewResource("mybucket", "*")), false},
{NewResourceSet(NewResource("mybucket*", "/myobject")), false},
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*")), false},
}
for i, testCase := range testCases {
result := testCase.resourceSet.bucketResourceExists()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceSetObjectResourceExists(t *testing.T) {
testCases := []struct {
resourceSet ResourceSet
expectedResult bool
}{
{NewResourceSet(NewResource("*", "")), true},
{NewResourceSet(NewResource("mybucket*", "")), true},
{NewResourceSet(NewResource("", "*")), true},
{NewResourceSet(NewResource("*", "*")), true},
{NewResourceSet(NewResource("mybucket", "*")), true},
{NewResourceSet(NewResource("mybucket*", "/myobject")), true},
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*")), true},
{NewResourceSet(NewResource("mybucket", ""), NewResource("mybucket", "/2910/photos/*")), true},
{NewResourceSet(NewResource("mybucket", "")), false},
{NewResourceSet(NewResource("mybucket?0", "")), false},
}
for i, testCase := range testCases {
result := testCase.resourceSet.objectResourceExists()
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceSetAdd(t *testing.T) {
testCases := []struct {
resourceSet ResourceSet
resource Resource
expectedResult ResourceSet
}{
{NewResourceSet(), NewResource("mybucket", "/myobject*"),
NewResourceSet(NewResource("mybucket", "/myobject*"))},
{NewResourceSet(NewResource("mybucket", "/myobject*")),
NewResource("mybucket", "/yourobject*"),
NewResourceSet(NewResource("mybucket", "/myobject*"),
NewResource("mybucket", "/yourobject*"))},
{NewResourceSet(NewResource("mybucket", "/myobject*")),
NewResource("mybucket", "/myobject*"),
NewResourceSet(NewResource("mybucket", "/myobject*"))},
}
for i, testCase := range testCases {
testCase.resourceSet.Add(testCase.resource)
if !reflect.DeepEqual(testCase.resourceSet, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, testCase.resourceSet)
}
}
}
func TestResourceSetIntersection(t *testing.T) {
testCases := []struct {
set ResourceSet
setToIntersect ResourceSet
expectedResult ResourceSet
}{
{NewResourceSet(), NewResourceSet(NewResource("mybucket", "/myobject*")), NewResourceSet()},
{NewResourceSet(NewResource("mybucket", "/myobject*")), NewResourceSet(), NewResourceSet()},
{NewResourceSet(NewResource("mybucket", "/myobject*")),
NewResourceSet(NewResource("mybucket", "/myobject*"), NewResource("mybucket", "/yourobject*")),
NewResourceSet(NewResource("mybucket", "/myobject*"))},
}
for i, testCase := range testCases {
result := testCase.set.Intersection(testCase.setToIntersect)
if !reflect.DeepEqual(result, testCase.expectedResult) {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set)
}
}
}
func TestResourceSetMarshalJSON(t *testing.T) {
testCases := []struct {
resoruceSet ResourceSet
expectedResult []byte
expectErr bool
}{
{NewResourceSet(NewResource("mybucket", "/myobject*")),
[]byte(`["arn:aws:s3:::mybucket/myobject*"]`), false},
{NewResourceSet(NewResource("mybucket", "/photos/myobject*")),
[]byte(`["arn:aws:s3:::mybucket/photos/myobject*"]`), false},
{NewResourceSet(), nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.resoruceSet)
expectErr := (err != nil)
if expectErr != testCase.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 TestResourceSetMatch(t *testing.T) {
testCases := []struct {
resourceSet ResourceSet
resource string
expectedResult bool
}{
{NewResourceSet(NewResource("*", "")), "mybucket", true},
{NewResourceSet(NewResource("*", "")), "mybucket/myobject", true},
{NewResourceSet(NewResource("mybucket*", "")), "mybucket", true},
{NewResourceSet(NewResource("mybucket*", "")), "mybucket/myobject", true},
{NewResourceSet(NewResource("", "*")), "/myobject", true},
{NewResourceSet(NewResource("*", "*")), "mybucket/myobject", true},
{NewResourceSet(NewResource("mybucket", "*")), "mybucket/myobject", true},
{NewResourceSet(NewResource("mybucket*", "/myobject")), "mybucket/myobject", true},
{NewResourceSet(NewResource("mybucket*", "/myobject")), "mybucket100/myobject", true},
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*")), "mybucket20/2010/photos/1.jpg", true},
{NewResourceSet(NewResource("mybucket", "")), "mybucket", true},
{NewResourceSet(NewResource("mybucket?0", "")), "mybucket30", true},
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*"),
NewResource("mybucket", "/2010/photos/*")), "mybucket/2010/photos/1.jpg", true},
{NewResourceSet(NewResource("", "*")), "mybucket/myobject", false},
{NewResourceSet(NewResource("*", "*")), "mybucket", false},
{NewResourceSet(NewResource("mybucket", "*")), "mybucket10/myobject", false},
{NewResourceSet(NewResource("mybucket", "")), "mybucket/myobject", false},
{NewResourceSet(), "mybucket/myobject", false},
}
for i, testCase := range testCases {
result := testCase.resourceSet.Match(testCase.resource)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
}
}
}
func TestResourceSetUnmarshalJSON(t *testing.T) {
testCases := []struct {
data []byte
expectedResult ResourceSet
expectErr bool
}{
{[]byte(`"arn:aws:s3:::mybucket/myobject*"`),
NewResourceSet(NewResource("mybucket", "/myobject*")), false},
{[]byte(`"arn:aws:s3:::mybucket/photos/myobject*"`),
NewResourceSet(NewResource("mybucket", "/photos/myobject*")), false},
{[]byte(`"arn:aws:s3:::mybucket"`), NewResourceSet(NewResource("mybucket", "")), false},
{[]byte(`"mybucket/myobject*"`), nil, true},
}
for i, testCase := range testCases {
var result ResourceSet
err := json.Unmarshal(testCase.data, &result)
expectErr := (err != nil)
if expectErr != testCase.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, testCase.expectedResult, result)
}
}
}
}
func TestResourceSetValidate(t *testing.T) {
testCases := []struct {
resourceSet ResourceSet
bucketName string
expectErr bool
}{
{NewResourceSet(NewResource("mybucket", "/myobject*")), "mybucket", false},
{NewResourceSet(NewResource("", "/myobject*")), "yourbucket", true},
{NewResourceSet(NewResource("mybucket", "/myobject*")), "yourbucket", true},
}
for i, testCase := range testCases {
err := testCase.resourceSet.Validate(testCase.bucketName)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
}
}

156
pkg/policy/statement.go Normal file
View File

@ -0,0 +1,156 @@
/*
* 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 policy
import (
"encoding/json"
"fmt"
"strings"
"github.com/minio/minio/pkg/policy/condition"
)
// Statement - policy statement.
type Statement struct {
SID ID `json:"Sid,omitempty"`
Effect Effect `json:"Effect"`
Principal Principal `json:"Principal"`
Actions ActionSet `json:"Action"`
Resources ResourceSet `json:"Resource"`
Conditions condition.Functions `json:"Condition,omitempty"`
}
// IsAllowed - checks given policy args is allowed to continue the Rest API.
func (statement Statement) IsAllowed(args Args) bool {
check := func() bool {
if !statement.Principal.Match(args.AccountName) {
return false
}
if !statement.Actions.Contains(args.Action) {
return false
}
resource := args.BucketName
if args.ObjectName != "" {
if !strings.HasPrefix(args.ObjectName, "/") {
resource += "/"
}
resource += args.ObjectName
}
if !statement.Resources.Match(resource) {
return false
}
return statement.Conditions.Evaluate(args.ConditionValues)
}
return statement.Effect.IsAllowed(check())
}
// isValid - checks whether statement is valid or not.
func (statement Statement) isValid() error {
if !statement.Effect.IsValid() {
return fmt.Errorf("invalid Effect %v", statement.Effect)
}
if !statement.Principal.IsValid() {
return fmt.Errorf("invalid Principal %v", statement.Principal)
}
if len(statement.Actions) == 0 {
return fmt.Errorf("Action must not be empty")
}
if len(statement.Resources) == 0 {
return fmt.Errorf("Resource must not be empty")
}
for action := range statement.Actions {
if action.isObjectAction() {
if !statement.Resources.objectResourceExists() {
return fmt.Errorf("unsupported Resource found %v for action %v", statement.Resources, action)
}
} else {
if !statement.Resources.bucketResourceExists() {
return fmt.Errorf("unsupported Resource found %v for action %v", statement.Resources, action)
}
}
keys := statement.Conditions.Keys()
keyDiff := keys.Difference(actionConditionKeyMap[action])
if !keyDiff.IsEmpty() {
return fmt.Errorf("unsupported condition keys '%v' used for action '%v'", keyDiff, action)
}
}
return nil
}
// MarshalJSON - encodes JSON data to Statement.
func (statement Statement) MarshalJSON() ([]byte, error) {
if err := statement.isValid(); err != nil {
return nil, err
}
// subtype to avoid recursive call to MarshalJSON()
type subStatement Statement
ss := subStatement(statement)
return json.Marshal(ss)
}
// UnmarshalJSON - decodes JSON data to Statement.
func (statement *Statement) UnmarshalJSON(data []byte) error {
// subtype to avoid recursive call to UnmarshalJSON()
type subStatement Statement
var ss subStatement
if err := json.Unmarshal(data, &ss); err != nil {
return err
}
s := Statement(ss)
if err := s.isValid(); err != nil {
return err
}
*statement = s
return nil
}
// Validate - validates Statement is for given bucket or not.
func (statement Statement) Validate(bucketName string) error {
if err := statement.isValid(); err != nil {
return err
}
return statement.Resources.Validate(bucketName)
}
// NewStatement - creates new statement.
func NewStatement(effect Effect, principal Principal, actionSet ActionSet, resourceSet ResourceSet, conditions condition.Functions) Statement {
return Statement{
Effect: effect,
Principal: principal,
Actions: actionSet,
Resources: resourceSet,
Conditions: conditions,
}
}

View File

@ -0,0 +1,571 @@
/*
* 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 policy
import (
"encoding/json"
"net"
"reflect"
"testing"
"github.com/minio/minio/pkg/policy/condition"
)
func TestStatementIsAllowed(t *testing.T) {
case1Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetBucketLocationAction, PutObjectAction),
NewResourceSet(NewResource("*", "")),
condition.NewFunctions(),
)
case2Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
)
_, IPNet1, err := net.ParseCIDR("192.168.1.0/24")
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func1, err := condition.NewIPAddressFunc(
condition.AWSSourceIP,
IPNet1,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func1),
)
case4Statement := NewStatement(
Deny,
NewPrincipal("*"),
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func1),
)
anonGetBucketLocationArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: GetBucketLocationAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{},
}
anonPutObjectActionArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: PutObjectAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{
"x-amz-copy-source": {"mybucket/myobject"},
"SourceIp": {"192.168.1.10"},
},
ObjectName: "myobject",
}
anonGetObjectActionArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: GetObjectAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{},
ObjectName: "myobject",
}
getBucketLocationArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: GetBucketLocationAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{},
IsOwner: true,
}
putObjectActionArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: PutObjectAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{
"x-amz-copy-source": {"mybucket/myobject"},
"SourceIp": {"192.168.1.10"},
},
IsOwner: true,
ObjectName: "myobject",
}
getObjectActionArgs := Args{
AccountName: "Q3AM3UQ867SPQQA43P2F",
Action: GetObjectAction,
BucketName: "mybucket",
ConditionValues: map[string][]string{},
IsOwner: true,
ObjectName: "myobject",
}
testCases := []struct {
statement Statement
args Args
expectedResult bool
}{
{case1Statement, anonGetBucketLocationArgs, true},
{case1Statement, anonPutObjectActionArgs, true},
{case1Statement, anonGetObjectActionArgs, false},
{case1Statement, getBucketLocationArgs, true},
{case1Statement, putObjectActionArgs, true},
{case1Statement, getObjectActionArgs, false},
{case2Statement, anonGetBucketLocationArgs, false},
{case2Statement, anonPutObjectActionArgs, true},
{case2Statement, anonGetObjectActionArgs, true},
{case2Statement, getBucketLocationArgs, false},
{case2Statement, putObjectActionArgs, true},
{case2Statement, getObjectActionArgs, true},
{case3Statement, anonGetBucketLocationArgs, false},
{case3Statement, anonPutObjectActionArgs, true},
{case3Statement, anonGetObjectActionArgs, false},
{case3Statement, getBucketLocationArgs, false},
{case3Statement, putObjectActionArgs, true},
{case3Statement, getObjectActionArgs, false},
{case4Statement, anonGetBucketLocationArgs, true},
{case4Statement, anonPutObjectActionArgs, false},
{case4Statement, anonGetObjectActionArgs, true},
{case4Statement, getBucketLocationArgs, true},
{case4Statement, putObjectActionArgs, false},
{case4Statement, getObjectActionArgs, true},
}
for i, testCase := range testCases {
result := testCase.statement.IsAllowed(testCase.args)
if result != testCase.expectedResult {
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
}
}
}
func TestStatementIsValid(t *testing.T) {
_, IPNet1, err := net.ParseCIDR("192.168.1.0/24")
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func1, err := condition.NewIPAddressFunc(
condition.AWSSourceIP,
IPNet1,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func2, err := condition.NewStringEqualsFunc(
condition.S3XAmzCopySource,
"mybucket/myobject",
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
testCases := []struct {
statement Statement
expectErr bool
}{
// Invalid effect error.
{NewStatement(
Effect("foo"),
NewPrincipal("*"),
NewActionSet(GetBucketLocationAction, PutObjectAction),
NewResourceSet(NewResource("*", "")),
condition.NewFunctions(),
), true},
// Invalid principal error.
{NewStatement(
Allow,
NewPrincipal(),
NewActionSet(GetBucketLocationAction, PutObjectAction),
NewResourceSet(NewResource("*", "")),
condition.NewFunctions(),
), true},
// Empty actions error.
{NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(),
NewResourceSet(NewResource("*", "")),
condition.NewFunctions(),
), true},
// Empty resources error.
{NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetBucketLocationAction, PutObjectAction),
NewResourceSet(),
condition.NewFunctions(),
), true},
// Unsupported resource found for object action.
{NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetBucketLocationAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "")),
condition.NewFunctions(),
), true},
// Unsupported resource found for bucket action.
{NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetBucketLocationAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "myobject*")),
condition.NewFunctions(),
), true},
// Unsupported condition key for action.
{NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "myobject*")),
condition.NewFunctions(func1, func2),
), true},
{NewStatement(
Deny,
NewPrincipal("*"),
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "myobject*")),
condition.NewFunctions(func1),
), false},
}
for i, testCase := range testCases {
err := testCase.statement.isValid()
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
}
}
func TestStatementMarshalJSON(t *testing.T) {
case1Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
)
case1Statement.SID = "SomeId1"
case1Data := []byte(`{"Sid":"SomeId1","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"]}`)
func1, err := condition.NewNullFunc(
condition.S3XAmzCopySource,
true,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func1),
)
case2Data := []byte(`{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"],"Condition":{"Null":{"s3:x-amz-copy-source":[true]}}}`)
func2, err := condition.NewNullFunc(
condition.S3XAmzServerSideEncryption,
false,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Statement := NewStatement(
Deny,
NewPrincipal("*"),
NewActionSet(GetObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func2),
)
case3Data := []byte(`{"Effect":"Deny","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"],"Condition":{"Null":{"s3:x-amz-server-side-encryption":[false]}}}`)
case4Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "myobject*")),
condition.NewFunctions(func1, func2),
)
testCases := []struct {
statement Statement
expectedResult []byte
expectErr bool
}{
{case1Statement, case1Data, false},
{case2Statement, case2Data, false},
{case3Statement, case3Data, false},
// Invalid statement error.
{case4Statement, nil, true},
}
for i, testCase := range testCases {
result, err := json.Marshal(testCase.statement)
expectErr := (err != nil)
if expectErr != testCase.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 TestStatementUnmarshalJSON(t *testing.T) {
case1Data := []byte(`{
"Sid": "SomeId1",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mybucket/myobject*"
}`)
case1Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
)
case1Statement.SID = "SomeId1"
case2Data := []byte(`{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mybucket/myobject*",
"Condition": {
"Null": {
"s3:x-amz-copy-source": true
}
}
}`)
func1, err := condition.NewNullFunc(
condition.S3XAmzCopySource,
true,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func1),
)
case3Data := []byte(`{
"Effect": "Deny",
"Principal": {
"AWS": "*"
},
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::mybucket/myobject*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption": "false"
}
}
}`)
func2, err := condition.NewNullFunc(
condition.S3XAmzServerSideEncryption,
false,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case3Statement := NewStatement(
Deny,
NewPrincipal("*"),
NewActionSet(PutObjectAction, GetObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(func2),
)
case4Data := []byte(`{
"Effect": "Allow",
"Principal": "Q3AM3UQ867SPQQA43P2F",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mybucket/myobject*"
}`)
case5Data := []byte(`{
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mybucket/myobject*"
}`)
case6Data := []byte(`{
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mybucket/myobject*"
}`)
case7Data := []byte(`{
"Effect": "Allow",
"Principal": "*",
"Resource": "arn:aws:s3:::mybucket/myobject*"
}`)
case8Data := []byte(`{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject"
}`)
case9Data := []byte(`{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mybucket/myobject*",
"Condition": {
}
}`)
case10Data := []byte(`{
"Effect": "Deny",
"Principal": {
"AWS": "*"
},
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::mybucket/myobject*",
"Condition": {
"StringEquals": {
"s3:x-amz-copy-source": "yourbucket/myobject*"
}
}
}`)
testCases := []struct {
data []byte
expectedResult Statement
expectErr bool
}{
{case1Data, case1Statement, false},
{case2Data, case2Statement, false},
{case3Data, case3Statement, false},
// JSON unmarshaling error.
{case4Data, Statement{}, true},
// Invalid effect error.
{case5Data, Statement{}, true},
// empty principal error.
{case6Data, Statement{}, true},
// Empty action error.
{case7Data, Statement{}, true},
// Empty resource error.
{case8Data, Statement{}, true},
// Empty condition error.
{case9Data, Statement{}, true},
// Unsupported condition key error.
{case10Data, Statement{}, true},
}
for i, testCase := range testCases {
var result Statement
err := json.Unmarshal(testCase.data, &result)
expectErr := (err != nil)
if expectErr != testCase.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, testCase.expectedResult, result)
}
}
}
}
func TestStatementValidate(t *testing.T) {
case1Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(PutObjectAction),
NewResourceSet(NewResource("mybucket", "/myobject*")),
condition.NewFunctions(),
)
func1, err := condition.NewNullFunc(
condition.S3XAmzCopySource,
true,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
func2, err := condition.NewNullFunc(
condition.S3XAmzServerSideEncryption,
false,
)
if err != nil {
t.Fatalf("unexpected error. %v\n", err)
}
case2Statement := NewStatement(
Allow,
NewPrincipal("*"),
NewActionSet(GetObjectAction, PutObjectAction),
NewResourceSet(NewResource("mybucket", "myobject*")),
condition.NewFunctions(func1, func2),
)
testCases := []struct {
statement Statement
bucketName string
expectErr bool
}{
{case1Statement, "mybucket", false},
{case2Statement, "mybucket", true},
{case1Statement, "yourbucket", true},
}
for i, testCase := range testCases {
err := testCase.statement.Validate(testCase.bucketName)
expectErr := (err != nil)
if expectErr != testCase.expectErr {
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
}
}
}