package api import ( "bytes" "encoding/json" "encoding/xml" "fmt" "io" "net/http" "net/http/httptest" "net/url" "strconv" "testing" "time" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" apiErrors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" s3middleware "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/metrics" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" engineiam "git.frostfs.info/TrueCloudLab/policy-engine/iam" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory" "git.frostfs.info/TrueCloudLab/policy-engine/schema/common" "git.frostfs.info/TrueCloudLab/policy-engine/schema/s3" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" ) type routerMock struct { t *testing.T router *chi.Mux cfg Config middlewareSettings *middlewareSettingsMock policyChecker engine.LocalOverrideEngine handler *handlerMock } func (m *routerMock) ServeHTTP(w http.ResponseWriter, r *http.Request) { m.router.ServeHTTP(w, r) } type option func(*Config) func frostFSIDValidation(flag bool) option { return func(cfg *Config) { cfg.FrostFSIDValidation = flag } } func prepareRouter(t *testing.T, opts ...option) *routerMock { middlewareSettings := &middlewareSettingsMock{} policyChecker := inmemory.NewInMemoryLocalOverrides() logger := zaptest.NewLogger(t) metricsConfig := metrics.AppMetricsConfig{ Logger: logger, PoolStatistics: &poolStatisticMock{}, Registerer: prometheus.NewRegistry(), Enabled: true, } handlerTestMock := &handlerMock{t: t, cfg: middlewareSettings, buckets: map[string]*data.BucketInfo{}} cfg := Config{ Throttle: middleware.ThrottleOpts{ Limit: 10, BacklogTimeout: 30 * time.Second, }, Handler: handlerTestMock, Center: ¢erMock{t: t}, Log: logger, Metrics: metrics.NewAppMetrics(metricsConfig), MiddlewareSettings: middlewareSettings, PolicyChecker: policyChecker, FrostfsID: &frostFSIDMock{}, XMLDecoder: &xmlMock{}, Tagging: &resourceTaggingMock{}, } for _, o := range opts { o(&cfg) } return &routerMock{ t: t, router: NewRouter(cfg), cfg: cfg, middlewareSettings: middlewareSettings, policyChecker: policyChecker, handler: handlerTestMock, } } func TestRouterUploadPart(t *testing.T) { chiRouter := prepareRouter(t) createBucket(chiRouter, "", "dkirillov") w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodPut, "/dkirillov/fix-object", nil) query := make(url.Values) query.Set("uploadId", "some-id") query.Set("partNumber", "1") r.URL.RawQuery = query.Encode() chiRouter.ServeHTTP(w, r) resp := readResponse(t, w) require.Equal(t, "UploadPart", resp.Method) } func TestRouterListMultipartUploads(t *testing.T) { chiRouter := prepareRouter(t) createBucket(chiRouter, "", "test-bucket") w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/test-bucket", nil) query := make(url.Values) query.Set("uploads", "") r.URL.RawQuery = query.Encode() chiRouter.ServeHTTP(w, r) resp := readResponse(t, w) require.Equal(t, "ListMultipartUploads", resp.Method) } func TestRouterObjectWithSlashes(t *testing.T) { chiRouter := prepareRouter(t) ns, bktName, objName := "", "dkirillov", "/fix/object" createBucket(chiRouter, ns, bktName) resp := putObject(chiRouter, ns, bktName, objName, nil) require.Equal(t, objName, resp.ReqInfo.ObjectName) } func TestRouterObjectEscaping(t *testing.T) { chiRouter := prepareRouter(t) ns, bktName := "", "dkirillov" createBucket(chiRouter, ns, bktName) for _, tc := range []struct { name string expectedObjName string objName string }{ { name: "simple", expectedObjName: "object", objName: "object", }, { name: "with slashes", expectedObjName: "fix/object", objName: "fix/object", }, { name: "with slash escaped", expectedObjName: "/foo/bar", objName: "/foo%2fbar", }, { name: "with percentage escaped", expectedObjName: "fix/object%ac", objName: "fix/object%25ac", }, { name: "with awful mint name", expectedObjName: "äöüex ®©µÄÆÐÕæŒƕƩDž 01000000 0x40 \u0040 amȡȹɆple&0a!-_.*'()&$@=;:+,?<>.pdf", objName: "%C3%A4%C3%B6%C3%BCex%20%C2%AE%C2%A9%C2%B5%C3%84%C3%86%C3%90%C3%95%C3%A6%C5%92%C6%95%C6%A9%C7%85%2001000000%200x40%20%40%20am%C8%A1%C8%B9%C9%86ple%260a%21-_.%2A%27%28%29%26%24%40%3D%3B%3A%2B%2C%3F%3C%3E.pdf", }, } { t.Run(tc.name, func(t *testing.T) { resp := putObject(chiRouter, ns, bktName, tc.objName, nil) require.Equal(t, tc.expectedObjName, resp.ReqInfo.ObjectName) }) } } func TestPolicyChecker(t *testing.T) { chiRouter := prepareRouter(t) ns1, bktName1, objName1 := "", "bucket", "object" ns2, bktName2, objName2 := "custom-ns", "other-bucket", "object" createBucket(chiRouter, ns1, bktName1) createBucket(chiRouter, ns2, bktName1) createBucket(chiRouter, ns2, bktName2) ruleChain := &chain.Chain{ ID: chain.ID("id"), Rules: []chain.Rule{{ Status: chain.AccessDenied, Actions: chain.Actions{Names: []string{"*"}}, Resources: chain.Resources{Names: []string{fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName1)}}, }}, } _, _, err := chiRouter.policyChecker.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, engine.NamespaceTarget(ns2), ruleChain) require.NoError(t, err) // check we can access 'bucket' in default namespace putObject(chiRouter, ns1, bktName1, objName1, nil) deleteObject(chiRouter, ns1, bktName1, objName1, nil) // check we can access 'other-bucket' in custom namespace putObject(chiRouter, ns2, bktName2, objName2, nil) deleteObject(chiRouter, ns2, bktName2, objName2, nil) // check we cannot access 'bucket' in custom namespace putObjectErr(chiRouter, ns2, bktName1, objName2, nil, apiErrors.ErrAccessDenied) deleteObjectErr(chiRouter, ns2, bktName1, objName2, nil, apiErrors.ErrAccessDenied) } func TestPolicyCheckerError(t *testing.T) { chiRouter := prepareRouter(t) ns1, bktName1, objName1 := "", "bucket", "object" putObjectErr(chiRouter, ns1, bktName1, objName1, nil, apiErrors.ErrNoSuchBucket) chiRouter = prepareRouter(t) chiRouter.cfg.FrostfsID.(*frostFSIDMock).userGroupsError = true putObjectErr(chiRouter, ns1, bktName1, objName1, nil, apiErrors.ErrInternalError) } func TestPolicyCheckerReqTypeDetermination(t *testing.T) { chiRouter := prepareRouter(t) bktName, objName := "bucket", "object" createBucket(chiRouter, "", bktName) policy := engineiam.Policy{ Version: "2012-10-17", Statement: []engineiam.Statement{{ Principal: map[engineiam.PrincipalType][]string{engineiam.Wildcard: {}}, Effect: engineiam.AllowEffect, Action: engineiam.Action{"s3:*"}, Resource: engineiam.Resource{fmt.Sprintf(s3.ResourceFormatS3All)}, }}, } ruleChain, err := engineiam.ConvertToS3Chain(policy, nil) require.NoError(t, err) _, _, err = chiRouter.policyChecker.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, engine.NamespaceTarget(""), ruleChain) require.NoError(t, err) createBucket(chiRouter, "", bktName) chiRouter.middlewareSettings.denyByDefault = true t.Run("can list buckets", func(t *testing.T) { w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil) chiRouter.ServeHTTP(w, r) resp := readResponse(t, w) require.Equal(t, s3middleware.ListBucketsOperation, resp.Method) }) t.Run("can head 'bucket'", func(t *testing.T) { w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodHead, "/"+bktName, nil) chiRouter.ServeHTTP(w, r) resp := readResponse(t, w) require.Equal(t, s3middleware.HeadBucketOperation, resp.Method) }) t.Run("can put object into 'bucket'", func(t *testing.T) { w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, fmt.Sprintf("/%s/%s", bktName, objName), nil) chiRouter.ServeHTTP(w, r) resp := readResponse(t, w) require.Equal(t, s3middleware.PutObjectOperation, resp.Method) }) } func TestDefaultBehaviorPolicyChecker(t *testing.T) { chiRouter := prepareRouter(t) ns, bktName := "", "bucket" // check we can access bucket if rules not found createBucket(chiRouter, ns, bktName) // check we cannot access if rules not found when settings is enabled chiRouter.middlewareSettings.denyByDefault = true createBucketErr(chiRouter, ns, bktName, nil, apiErrors.ErrAccessDenied) } func TestDefaultPolicyCheckerWithUserTags(t *testing.T) { router := prepareRouter(t) ns, bktName := "", "bucket" router.middlewareSettings.denyByDefault = true allowOperations(router, ns, []string{"s3:CreateBucket"}, engineiam.Conditions{ engineiam.CondStringEquals: engineiam.Condition{fmt.Sprintf(common.PropertyKeyFormatFrostFSIDUserClaim, "tag-test"): []string{"test"}}, }) createBucketErr(router, ns, bktName, nil, apiErrors.ErrAccessDenied) tags := make(map[string]string) tags["tag-test"] = "test" router.cfg.FrostfsID.(*frostFSIDMock).tags = tags createBucket(router, ns, bktName) } func TestRequestParametersCheck(t *testing.T) { t.Run("prefix parameter, allow specific value", func(t *testing.T) { router := prepareRouter(t) ns, bktName, prefix := "", "bucket", "prefix" router.middlewareSettings.denyByDefault = true allowOperations(router, ns, []string{"s3:CreateBucket"}, nil) createBucket(router, ns, bktName) // Add policies and check denyOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{ engineiam.CondStringNotEquals: engineiam.Condition{s3.PropertyKeyPrefix: []string{prefix}}, }) allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{ engineiam.CondStringEquals: engineiam.Condition{s3.PropertyKeyPrefix: []string{prefix}}, }) listObjectsV1(router, ns, bktName, prefix, "", "") listObjectsV1Err(router, ns, bktName, "", "", "", apiErrors.ErrAccessDenied) listObjectsV1Err(router, ns, bktName, "invalid", "", "", apiErrors.ErrAccessDenied) }) t.Run("delimiter parameter, prohibit specific value", func(t *testing.T) { router := prepareRouter(t) ns, bktName, delimiter := "", "bucket", "delimiter" router.middlewareSettings.denyByDefault = true allowOperations(router, ns, []string{"s3:CreateBucket"}, nil) createBucket(router, ns, bktName) // Add policies and check denyOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{ engineiam.CondStringEquals: engineiam.Condition{s3.PropertyKeyDelimiter: []string{delimiter}}, }) allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{ engineiam.CondStringNotEquals: engineiam.Condition{s3.PropertyKeyDelimiter: []string{delimiter}}, }) listObjectsV1(router, ns, bktName, "", "", "") listObjectsV1(router, ns, bktName, "", "some-delimiter", "") listObjectsV1Err(router, ns, bktName, "", delimiter, "", apiErrors.ErrAccessDenied) }) t.Run("max-keys parameter, allow specific value", func(t *testing.T) { router := prepareRouter(t) ns, bktName, maxKeys := "", "bucket", 10 router.middlewareSettings.denyByDefault = true allowOperations(router, ns, []string{"s3:CreateBucket"}, nil) createBucket(router, ns, bktName) // Add policies and check denyOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{ engineiam.CondNumericNotEquals: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}}, }) allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{ engineiam.CondNumericEquals: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}}, }) listObjectsV1(router, ns, bktName, "", "", strconv.Itoa(maxKeys)) listObjectsV1Err(router, ns, bktName, "", "", "", apiErrors.ErrAccessDenied) listObjectsV1Err(router, ns, bktName, "", "", strconv.Itoa(maxKeys-1), apiErrors.ErrAccessDenied) listObjectsV1Err(router, ns, bktName, "", "", "invalid", apiErrors.ErrAccessDenied) }) t.Run("max-keys parameter, allow range of values", func(t *testing.T) { router := prepareRouter(t) ns, bktName, maxKeys := "", "bucket", 10 router.middlewareSettings.denyByDefault = true allowOperations(router, ns, []string{"s3:CreateBucket"}, nil) createBucket(router, ns, bktName) // Add policies and check denyOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{ engineiam.CondNumericGreaterThan: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}}, }) allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{ engineiam.CondNumericLessThanEquals: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}}, }) listObjectsV1(router, ns, bktName, "", "", strconv.Itoa(maxKeys)) listObjectsV1(router, ns, bktName, "", "", strconv.Itoa(maxKeys-1)) listObjectsV1Err(router, ns, bktName, "", "", strconv.Itoa(maxKeys+1), apiErrors.ErrAccessDenied) }) t.Run("max-keys parameter, prohibit specific value", func(t *testing.T) { router := prepareRouter(t) ns, bktName, maxKeys := "", "bucket", 10 router.middlewareSettings.denyByDefault = true allowOperations(router, ns, []string{"s3:CreateBucket"}, nil) createBucket(router, ns, bktName) // Add policies and check denyOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{ engineiam.CondNumericEquals: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}}, }) allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{ engineiam.CondNumericNotEquals: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}}, }) listObjectsV1(router, ns, bktName, "", "", "") listObjectsV1(router, ns, bktName, "", "", strconv.Itoa(maxKeys-1)) listObjectsV1Err(router, ns, bktName, "", "", strconv.Itoa(maxKeys), apiErrors.ErrAccessDenied) }) } func TestRequestTagsCheck(t *testing.T) { t.Run("put bucket tagging", func(t *testing.T) { router := prepareRouter(t) ns, bktName, tagKey, tagValue := "", "bucket", "tag", "value" router.middlewareSettings.denyByDefault = true allowOperations(router, ns, []string{"s3:CreateBucket"}, nil) createBucket(router, ns, bktName) // Add policies and check allowOperations(router, ns, []string{"s3:PutBucketTagging"}, engineiam.Conditions{ engineiam.CondStringEquals: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatRequestTag, tagKey): []string{tagValue}}, }) denyOperations(router, ns, []string{"s3:PutBucketTagging"}, engineiam.Conditions{ engineiam.CondStringNotEquals: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatRequestTag, tagKey): []string{tagValue}}, }) tagging, err := xml.Marshal(data.Tagging{TagSet: []data.Tag{{Key: tagKey, Value: tagValue}}}) require.NoError(t, err) putBucketTagging(router, ns, bktName, tagging) tagging, err = xml.Marshal(data.Tagging{TagSet: []data.Tag{{Key: "key", Value: tagValue}}}) require.NoError(t, err) putBucketTaggingErr(router, ns, bktName, tagging, apiErrors.ErrAccessDenied) tagging = nil putBucketTaggingErr(router, ns, bktName, tagging, apiErrors.ErrMalformedXML) }) t.Run("put object with tag", func(t *testing.T) { router := prepareRouter(t) ns, bktName, objName, tagKey, tagValue := "", "bucket", "object", "tag", "value" router.middlewareSettings.denyByDefault = true allowOperations(router, ns, []string{"s3:CreateBucket"}, nil) createBucket(router, ns, bktName) // Add policies and check allowOperations(router, ns, []string{"s3:PutObject"}, engineiam.Conditions{ engineiam.CondStringEquals: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatRequestTag, tagKey): []string{tagValue}}, }) denyOperations(router, ns, []string{"s3:PutObject"}, engineiam.Conditions{ engineiam.CondStringNotEquals: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatRequestTag, tagKey): []string{tagValue}}, }) putObject(router, ns, bktName, objName, &data.Tag{Key: tagKey, Value: tagValue}) putObjectErr(router, ns, bktName, objName, &data.Tag{Key: "key", Value: tagValue}, apiErrors.ErrAccessDenied) }) } func TestResourceTagsCheck(t *testing.T) { t.Run("bucket tagging", func(t *testing.T) { router := prepareRouter(t) ns, bktName, tagKey, tagValue := "", "bucket", "tag", "value" router.middlewareSettings.denyByDefault = true allowOperations(router, ns, []string{"s3:CreateBucket"}, nil) createBucket(router, ns, bktName) // Add policies and check allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{ engineiam.CondStringEquals: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatResourceTag, tagKey): []string{tagValue}}, }) denyOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{ engineiam.CondStringNotEquals: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatResourceTag, tagKey): []string{tagValue}}, }) router.cfg.Tagging.(*resourceTaggingMock).bucketTags = map[string]string{tagKey: tagValue} listObjectsV1(router, ns, bktName, "", "", "") router.cfg.Tagging.(*resourceTaggingMock).bucketTags = map[string]string{} listObjectsV1Err(router, ns, bktName, "", "", "", apiErrors.ErrAccessDenied) }) t.Run("object tagging", func(t *testing.T) { router := prepareRouter(t) ns, bktName, objName, tagKey, tagValue := "", "bucket", "object", "tag", "value" router.middlewareSettings.denyByDefault = true allowOperations(router, ns, []string{"s3:CreateBucket", "s3:PutObject"}, nil) createBucket(router, ns, bktName) putObject(router, ns, bktName, objName, nil) // Add policies and check allowOperations(router, ns, []string{"s3:GetObject"}, engineiam.Conditions{ engineiam.CondStringEquals: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatResourceTag, tagKey): []string{tagValue}}, }) denyOperations(router, ns, []string{"s3:GetObject"}, engineiam.Conditions{ engineiam.CondStringNotEquals: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatResourceTag, tagKey): []string{tagValue}}, }) router.cfg.Tagging.(*resourceTaggingMock).objectTags = map[string]string{tagKey: tagValue} getObject(router, ns, bktName, objName) router.cfg.Tagging.(*resourceTaggingMock).objectTags = map[string]string{} getObjectErr(router, ns, bktName, objName, apiErrors.ErrAccessDenied) }) t.Run("non-existent resources", func(t *testing.T) { router := prepareRouter(t) ns, bktName, objName := "", "bucket", "object" listObjectsV1Err(router, ns, bktName, "", "", "", apiErrors.ErrNoSuchBucket) router.cfg.Tagging.(*resourceTaggingMock).noSuchBucketKey = true createBucket(router, ns, bktName) getBucketErr(router, ns, bktName, apiErrors.ErrNoSuchKey) router.cfg.Tagging.(*resourceTaggingMock).noSuchObjectKey = true createBucket(router, ns, bktName) getObjectErr(router, ns, bktName, objName, apiErrors.ErrNoSuchKey) }) } func TestAccessBoxAttributesCheck(t *testing.T) { router := prepareRouter(t) ns, bktName, attrKey, attrValue := "", "bucket", "key", "true" router.middlewareSettings.denyByDefault = true allowOperations(router, ns, []string{"s3:CreateBucket"}, nil) createBucket(router, ns, bktName) // Add policy and check allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{ engineiam.CondBool: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatAccessBoxAttr, attrKey): []string{attrValue}}, }) listObjectsV1Err(router, ns, bktName, "", "", "", apiErrors.ErrAccessDenied) var attr object.Attribute attr.SetKey(attrKey) attr.SetValue(attrValue) router.cfg.Center.(*centerMock).attrs = []object.Attribute{attr} listObjectsV1(router, ns, bktName, "", "", "") } func TestSourceIPCheck(t *testing.T) { router := prepareRouter(t) ns, bktName, hdr := "", "bucket", "Source-Ip" router.middlewareSettings.denyByDefault = true // Add policy and check allowOperations(router, ns, []string{"s3:CreateBucket"}, engineiam.Conditions{ engineiam.CondIPAddress: engineiam.Condition{"aws:SourceIp": []string{"192.0.2.0/24"}}, }) router.middlewareSettings.sourceIPHeader = hdr header := map[string][]string{hdr: {"192.0.3.0"}} createBucketErr(router, ns, bktName, header, apiErrors.ErrAccessDenied) router.middlewareSettings.sourceIPHeader = "" createBucket(router, ns, bktName) } func TestMFAPolicy(t *testing.T) { router := prepareRouter(t) ns, bktName := "", "bucket" router.middlewareSettings.denyByDefault = true allowOperations(router, ns, []string{"s3:CreateBucket"}, nil) denyOperations(router, ns, []string{"s3:CreateBucket"}, engineiam.Conditions{ engineiam.CondBool: engineiam.Condition{s3.PropertyKeyAccessBoxAttrMFA: []string{"false"}}, }) createBucketErr(router, ns, bktName, nil, apiErrors.ErrAccessDenied) var attr object.Attribute attr.SetKey("IAM-MFA") attr.SetValue("true") router.cfg.Center.(*centerMock).attrs = []object.Attribute{attr} createBucket(router, ns, bktName) } func allowOperations(router *routerMock, ns string, operations []string, conditions engineiam.Conditions) { addPolicy(router, ns, "allow", engineiam.AllowEffect, operations, conditions) } func denyOperations(router *routerMock, ns string, operations []string, conditions engineiam.Conditions) { addPolicy(router, ns, "deny", engineiam.DenyEffect, operations, conditions) } func addPolicy(router *routerMock, ns string, id string, effect engineiam.Effect, operations []string, conditions engineiam.Conditions) { policy := engineiam.Policy{ Version: "2012-10-17", Statement: []engineiam.Statement{{ Principal: map[engineiam.PrincipalType][]string{engineiam.Wildcard: {}}, Effect: effect, Action: engineiam.Action(operations), Resource: engineiam.Resource{fmt.Sprintf(s3.ResourceFormatS3All)}, Conditions: conditions, }}, } ruleChain, err := engineiam.ConvertToS3Chain(policy, nil) require.NoError(router.t, err) ruleChain.ID = chain.ID(id) _, _, err = router.policyChecker.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, engine.NamespaceTarget(ns), ruleChain) require.NoError(router.t, err) } func createBucket(router *routerMock, namespace, bktName string) { w := createBucketBase(router, namespace, bktName, nil) resp := readResponse(router.t, w) require.Equal(router.t, s3middleware.CreateBucketOperation, resp.Method) } func createBucketErr(router *routerMock, namespace, bktName string, header http.Header, errCode apiErrors.ErrorCode) { w := createBucketBase(router, namespace, bktName, header) assertAPIError(router.t, w, errCode) } func createBucketBase(router *routerMock, namespace, bktName string, header http.Header) *httptest.ResponseRecorder { w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, "/"+bktName, nil) r.Header.Set(FrostfsNamespaceHeader, namespace) for key := range header { r.Header.Set(key, header.Get(key)) } router.ServeHTTP(w, r) return w } func getBucketErr(router *routerMock, namespace, bktName string, errCode apiErrors.ErrorCode) { w := getBucketBase(router, namespace, bktName) assertAPIError(router.t, w, errCode) } func getBucketBase(router *routerMock, namespace, bktName string) *httptest.ResponseRecorder { w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/"+bktName, nil) r.Header.Set(FrostfsNamespaceHeader, namespace) router.ServeHTTP(w, r) return w } func putObject(router *routerMock, namespace, bktName, objName string, tag *data.Tag) handlerResult { w := putObjectBase(router, namespace, bktName, objName, tag) resp := readResponse(router.t, w) require.Equal(router.t, s3middleware.PutObjectOperation, resp.Method) return resp } func putObjectErr(router *routerMock, namespace, bktName, objName string, tag *data.Tag, errCode apiErrors.ErrorCode) { w := putObjectBase(router, namespace, bktName, objName, tag) assertAPIError(router.t, w, errCode) } func putObjectBase(router *routerMock, namespace, bktName, objName string, tag *data.Tag) *httptest.ResponseRecorder { w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, "/"+bktName+"/"+objName, nil) if tag != nil { queries := url.Values{ tag.Key: []string{tag.Value}, } r.Header.Set(AmzTagging, queries.Encode()) } r.Header.Set(FrostfsNamespaceHeader, namespace) router.ServeHTTP(w, r) return w } func deleteObject(router *routerMock, namespace, bktName, objName string, tag *data.Tag) handlerResult { w := deleteObjectBase(router, namespace, bktName, objName, tag) resp := readResponse(router.t, w) require.Equal(router.t, s3middleware.DeleteObjectOperation, resp.Method) return resp } func deleteObjectErr(router *routerMock, namespace, bktName, objName string, tag *data.Tag, errCode apiErrors.ErrorCode) { w := deleteObjectBase(router, namespace, bktName, objName, tag) assertAPIError(router.t, w, errCode) } func deleteObjectBase(router *routerMock, namespace, bktName, objName string, tag *data.Tag) *httptest.ResponseRecorder { w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodDelete, "/"+bktName+"/"+objName, nil) if tag != nil { queries := url.Values{ tag.Key: []string{tag.Value}, } r.Header.Set(AmzTagging, queries.Encode()) } r.Header.Set(FrostfsNamespaceHeader, namespace) router.ServeHTTP(w, r) return w } func putBucketTagging(router *routerMock, namespace, bktName string, tagging []byte) handlerResult { w := putBucketTaggingBase(router, namespace, bktName, tagging) resp := readResponse(router.t, w) require.Equal(router.t, s3middleware.PutBucketTaggingOperation, resp.Method) return resp } func putBucketTaggingErr(router *routerMock, namespace, bktName string, tagging []byte, errCode apiErrors.ErrorCode) { w := putBucketTaggingBase(router, namespace, bktName, tagging) assertAPIError(router.t, w, errCode) } func putBucketTaggingBase(router *routerMock, namespace, bktName string, tagging []byte) *httptest.ResponseRecorder { queries := url.Values{} queries.Add(s3middleware.TaggingQuery, "") w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, "/"+bktName, bytes.NewBuffer(tagging)) r.URL.RawQuery = queries.Encode() r.Header.Set(FrostfsNamespaceHeader, namespace) router.ServeHTTP(w, r) return w } func getObject(router *routerMock, namespace, bktName, objName string) handlerResult { w := getObjectBase(router, namespace, bktName, objName) resp := readResponse(router.t, w) require.Equal(router.t, s3middleware.GetObjectOperation, resp.Method) return resp } func getObjectErr(router *routerMock, namespace, bktName, objName string, errCode apiErrors.ErrorCode) { w := getObjectBase(router, namespace, bktName, objName) assertAPIError(router.t, w, errCode) } func getObjectBase(router *routerMock, namespace, bktName, objName string) *httptest.ResponseRecorder { w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/"+bktName+"/"+objName, nil) r.Header.Set(FrostfsNamespaceHeader, namespace) router.ServeHTTP(w, r) return w } func listObjectsV1(router *routerMock, namespace, bktName, prefix, delimiter, maxKeys string) handlerResult { w := listObjectsV1Base(router, namespace, bktName, prefix, delimiter, maxKeys) resp := readResponse(router.t, w) require.Equal(router.t, s3middleware.ListObjectsV1Operation, resp.Method) return resp } func listObjectsV1Err(router *routerMock, namespace, bktName, prefix, delimiter, maxKeys string, errCode apiErrors.ErrorCode) { w := listObjectsV1Base(router, namespace, bktName, prefix, delimiter, maxKeys) assertAPIError(router.t, w, errCode) } func listObjectsV1Base(router *routerMock, namespace, bktName, prefix, delimiter, maxKeys string) *httptest.ResponseRecorder { queries := url.Values{} if len(prefix) > 0 { queries.Add(s3middleware.QueryPrefix, prefix) } if len(delimiter) > 0 { queries.Add(s3middleware.QueryDelimiter, delimiter) } if len(maxKeys) > 0 { queries.Add(s3middleware.QueryMaxKeys, maxKeys) } encoded := queries.Encode() w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/"+bktName, nil) r.URL.RawQuery = encoded r.Header.Set(FrostfsNamespaceHeader, namespace) router.ServeHTTP(w, r) return w } func TestOwnerIDRetrieving(t *testing.T) { chiRouter := prepareRouter(t) ns, bktName, objName := "", "test-bucket", "test-object" createBucket(chiRouter, ns, bktName) resp := putObject(chiRouter, ns, bktName, objName, nil) require.NotEqual(t, "anon", resp.ReqInfo.User) chiRouter.cfg.Center.(*centerMock).anon = true resp = putObject(chiRouter, ns, bktName, objName, nil) require.Equal(t, "anon", resp.ReqInfo.User) } func TestBillingMetrics(t *testing.T) { chiRouter := prepareRouter(t) ns, bktName, objName := "", "test-bucket", "test-object" createBucket(chiRouter, ns, bktName) dump := chiRouter.cfg.Metrics.UsersAPIStats().DumpMetrics() require.Len(t, dump.Requests, 1) require.NotEqual(t, "anon", dump.Requests[0].User) require.Equal(t, metrics.PUTRequest, dump.Requests[0].Operation) require.Equal(t, bktName, dump.Requests[0].Bucket) require.Equal(t, 1, dump.Requests[0].Requests) chiRouter.cfg.Center.(*centerMock).anon = true putObject(chiRouter, ns, bktName, objName, nil) dump = chiRouter.cfg.Metrics.UsersAPIStats().DumpMetrics() require.Len(t, dump.Requests, 1) require.Equal(t, "anon", dump.Requests[0].User) } func TestAuthenticate(t *testing.T) { chiRouter := prepareRouter(t) createBucket(chiRouter, "", "bkt-1") chiRouter = prepareRouter(t) chiRouter.cfg.Center.(*centerMock).noAuthHeader = true createBucket(chiRouter, "", "bkt-2") chiRouter = prepareRouter(t) chiRouter.cfg.Center.(*centerMock).isError = true createBucketErr(chiRouter, "", "bkt-3", nil, apiErrors.ErrAccessDenied) } func TestFrostFSIDValidation(t *testing.T) { // successful frostFSID validation chiRouter := prepareRouter(t, frostFSIDValidation(true)) createBucket(chiRouter, "", "bkt-1") // anon request, skip frostFSID validation chiRouter = prepareRouter(t, frostFSIDValidation(true)) chiRouter.cfg.Center.(*centerMock).anon = true createBucket(chiRouter, "", "bkt-2") // frostFSID validation failed chiRouter = prepareRouter(t, frostFSIDValidation(true)) chiRouter.cfg.FrostfsID.(*frostFSIDMock).validateError = true createBucketErr(chiRouter, "", "bkt-3", nil, apiErrors.ErrInternalError) } func TestRouterListObjectsV2Domains(t *testing.T) { chiRouter := prepareRouter(t, enableVHSDomains("domain.com")) chiRouter.handler.buckets["bucket"] = &data.BucketInfo{} w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/", nil) r.Host = "bucket.domain.com" query := make(url.Values) query.Set(s3middleware.ListTypeQuery, "2") r.URL.RawQuery = query.Encode() chiRouter.ServeHTTP(w, r) resp := readResponse(t, w) require.Equal(t, s3middleware.ListObjectsV2Operation, resp.Method) } func enableVHSDomains(domains ...string) option { return func(cfg *Config) { setting := cfg.MiddlewareSettings.(*middlewareSettingsMock) setting.vhsEnabled = true setting.domains = domains } } func readResponse(t *testing.T, w *httptest.ResponseRecorder) handlerResult { var res handlerResult resData, err := io.ReadAll(w.Result().Body) require.NoError(t, err) err = json.Unmarshal(resData, &res) require.NoErrorf(t, err, "actual body: '%s'", string(resData)) return res } func assertAPIError(t *testing.T, w *httptest.ResponseRecorder, expectedErrorCode apiErrors.ErrorCode) { actualErrorResponse := &s3middleware.ErrorResponse{} err := xml.NewDecoder(w.Result().Body).Decode(actualErrorResponse) require.NoError(t, err) expectedError := apiErrors.GetAPIError(expectedErrorCode) require.Equal(t, expectedError.HTTPStatusCode, w.Code) require.Equal(t, expectedError.Code, actualErrorResponse.Code) if expectedError.ErrCode != apiErrors.ErrInternalError { require.Contains(t, actualErrorResponse.Message, expectedError.Description) } }