forked from TrueCloudLab/frostfs-s3-gw
938 lines
33 KiB
Go
938 lines
33 KiB
Go
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"
|
|
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs"
|
|
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/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
"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, apierr.ErrAccessDenied)
|
|
deleteObjectErr(chiRouter, ns2, bktName1, objName2, nil, apierr.ErrAccessDenied)
|
|
}
|
|
|
|
func TestPolicyCheckerError(t *testing.T) {
|
|
chiRouter := prepareRouter(t)
|
|
ns1, bktName1, objName1 := "", "bucket", "object"
|
|
putObjectErr(chiRouter, ns1, bktName1, objName1, nil, apierr.ErrNoSuchBucket)
|
|
|
|
chiRouter = prepareRouter(t)
|
|
chiRouter.cfg.FrostfsID.(*frostFSIDMock).userGroupsError = true
|
|
putObjectErr(chiRouter, ns1, bktName1, objName1, nil, apierr.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 TestPolicyCheckFrostfsErrors(t *testing.T) {
|
|
chiRouter := prepareRouter(t)
|
|
ns1, bktName1, objName1 := "", "bucket", "object"
|
|
|
|
createBucket(chiRouter, ns1, bktName1)
|
|
key, err := keys.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
chiRouter.cfg.Center.(*centerMock).key = key
|
|
chiRouter.cfg.MiddlewareSettings.(*middlewareSettingsMock).denyByDefault = true
|
|
|
|
ruleChain := &chain.Chain{
|
|
ID: chain.ID("id"),
|
|
Rules: []chain.Rule{{
|
|
Status: chain.Allow,
|
|
Actions: chain.Actions{Names: []string{"*"}},
|
|
Resources: chain.Resources{Names: []string{fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName1)}},
|
|
}},
|
|
}
|
|
|
|
_, _, err = chiRouter.policyChecker.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, engine.UserTarget(ns1+":"+key.Address()), ruleChain)
|
|
require.NoError(t, err)
|
|
|
|
// check we can access 'bucket' in default namespace
|
|
putObject(chiRouter, ns1, bktName1, objName1, nil)
|
|
|
|
chiRouter.cfg.Center.(*centerMock).anon = true
|
|
chiRouter.cfg.Tagging.(*resourceTaggingMock).err = frostfs.ErrAccessDenied
|
|
getObjectErr(chiRouter, ns1, bktName1, objName1, apierr.ErrAccessDenied)
|
|
}
|
|
|
|
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, apierr.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, apierr.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, "", "", "", apierr.ErrAccessDenied)
|
|
listObjectsV1Err(router, ns, bktName, "invalid", "", "", apierr.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, "", apierr.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, "", "", "", apierr.ErrAccessDenied)
|
|
listObjectsV1Err(router, ns, bktName, "", "", strconv.Itoa(maxKeys-1), apierr.ErrAccessDenied)
|
|
listObjectsV1Err(router, ns, bktName, "", "", "invalid", apierr.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), apierr.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), apierr.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, apierr.ErrAccessDenied)
|
|
|
|
tagging = nil
|
|
putBucketTaggingErr(router, ns, bktName, tagging, apierr.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}, apierr.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, "", "", "", apierr.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, apierr.ErrAccessDenied)
|
|
})
|
|
|
|
t.Run("non-existent resources", func(t *testing.T) {
|
|
router := prepareRouter(t)
|
|
ns, bktName, objName := "", "bucket", "object"
|
|
|
|
listObjectsV1Err(router, ns, bktName, "", "", "", apierr.ErrNoSuchBucket)
|
|
|
|
router.cfg.Tagging.(*resourceTaggingMock).err = apierr.GetAPIError(apierr.ErrNoSuchKey)
|
|
createBucket(router, ns, bktName)
|
|
getBucketErr(router, ns, bktName, apierr.ErrNoSuchKey)
|
|
|
|
createBucket(router, ns, bktName)
|
|
getObjectErr(router, ns, bktName, objName, apierr.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, "", "", "", apierr.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, apierr.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, apierr.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 apierr.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 apierr.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 apierr.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 apierr.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 apierr.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 apierr.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 apierr.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).err = apierr.GetAPIError(apierr.ErrAccessDenied)
|
|
createBucketErr(chiRouter, "", "bkt-3", nil, apierr.ErrAccessDenied)
|
|
|
|
chiRouter.cfg.Center.(*centerMock).err = frostfs.ErrGatewayTimeout
|
|
createBucketErr(chiRouter, "", "bkt-3", nil, apierr.ErrGatewayTimeout)
|
|
|
|
chiRouter.cfg.Center.(*centerMock).err = apierr.GetAPIError(apierr.ErrInternalError)
|
|
createBucketErr(chiRouter, "", "bkt-3", nil, apierr.ErrAccessDenied)
|
|
|
|
chiRouter.cfg.Center.(*centerMock).err = apierr.GetAPIError(apierr.ErrBadRequest)
|
|
createBucketErr(chiRouter, "", "bkt-3", nil, apierr.ErrBadRequest)
|
|
}
|
|
|
|
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, apierr.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 apierr.ErrorCode) {
|
|
actualErrorResponse := &s3middleware.ErrorResponse{}
|
|
err := xml.NewDecoder(w.Result().Body).Decode(actualErrorResponse)
|
|
require.NoError(t, err)
|
|
|
|
expectedError := apierr.GetAPIError(expectedErrorCode)
|
|
|
|
require.Equal(t, expectedError.HTTPStatusCode, w.Code)
|
|
require.Equal(t, expectedError.Code, actualErrorResponse.Code)
|
|
|
|
if expectedError.ErrCode != apierr.ErrInternalError {
|
|
require.Contains(t, actualErrorResponse.Message, expectedError.Description)
|
|
}
|
|
}
|