forked from TrueCloudLab/frostfs-s3-gw
280 lines
8.8 KiB
Go
280 lines
8.8 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
|
|
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"
|
|
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/s3"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap/zaptest"
|
|
)
|
|
|
|
type routerMock struct {
|
|
router *chi.Mux
|
|
cfg Config
|
|
middlewareSettings *middlewareSettingsMock
|
|
policyChecker engine.LocalOverrideEngine
|
|
}
|
|
|
|
func (m *routerMock) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
m.router.ServeHTTP(w, r)
|
|
}
|
|
|
|
func prepareRouter(t *testing.T) *routerMock {
|
|
middlewareSettings := &middlewareSettingsMock{}
|
|
policyChecker := inmemory.NewInMemoryLocalOverrides()
|
|
|
|
cfg := Config{
|
|
Throttle: middleware.ThrottleOpts{
|
|
Limit: 10,
|
|
BacklogTimeout: 30 * time.Second,
|
|
},
|
|
Handler: &handlerMock{t: t},
|
|
Center: ¢erMock{},
|
|
Log: zaptest.NewLogger(t),
|
|
Metrics: &metrics.AppMetrics{},
|
|
MiddlewareSettings: middlewareSettings,
|
|
PolicyChecker: policyChecker,
|
|
Domains: []string{"domain1", "domain2"},
|
|
}
|
|
return &routerMock{
|
|
router: NewRouter(cfg),
|
|
cfg: cfg,
|
|
middlewareSettings: middlewareSettings,
|
|
policyChecker: policyChecker,
|
|
}
|
|
}
|
|
|
|
func TestRouterUploadPart(t *testing.T) {
|
|
chiRouter := prepareRouter(t)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
bktName, objName := "dkirillov", "/fix/object"
|
|
target := fmt.Sprintf("/%s/%s", bktName, objName)
|
|
|
|
w := httptest.NewRecorder()
|
|
r := httptest.NewRequest(http.MethodPut, target, nil)
|
|
|
|
chiRouter.ServeHTTP(w, r)
|
|
resp := readResponse(t, w)
|
|
require.Equal(t, "PutObject", resp.Method)
|
|
require.Equal(t, objName, resp.ReqInfo.ObjectName)
|
|
}
|
|
|
|
func TestRouterObjectEscaping(t *testing.T) {
|
|
chiRouter := prepareRouter(t)
|
|
|
|
bktName := "dkirillov"
|
|
|
|
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) {
|
|
target := fmt.Sprintf("/%s/%s", bktName, tc.objName)
|
|
|
|
w := httptest.NewRecorder()
|
|
r := httptest.NewRequest(http.MethodPut, target, nil)
|
|
|
|
chiRouter.ServeHTTP(w, r)
|
|
resp := readResponse(t, w)
|
|
require.Equal(t, "PutObject", resp.Method)
|
|
require.Equal(t, tc.expectedObjName, resp.ReqInfo.ObjectName)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPolicyChecker(t *testing.T) {
|
|
chiRouter := prepareRouter(t)
|
|
namespace := "custom-ns"
|
|
bktName, objName := "bucket", "object"
|
|
target := fmt.Sprintf("/%s/%s", bktName, objName)
|
|
|
|
ruleChain := &chain.Chain{
|
|
ID: "id",
|
|
Rules: []chain.Rule{{
|
|
Status: chain.AccessDenied,
|
|
Actions: chain.Actions{Names: []string{"*"}},
|
|
Resources: chain.Resources{Names: []string{fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName)}},
|
|
}},
|
|
}
|
|
|
|
_, _, err := chiRouter.policyChecker.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, engine.NamespaceTarget(namespace), ruleChain)
|
|
require.NoError(t, err)
|
|
|
|
// check we can access 'bucket' in default namespace
|
|
w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, target, nil)
|
|
chiRouter.ServeHTTP(w, r)
|
|
resp := readResponse(t, w)
|
|
require.Equal(t, s3middleware.PutObjectOperation, resp.Method)
|
|
|
|
// check we can access 'other-bucket' in custom namespace
|
|
w, r = httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, "/other-bucket/object", nil)
|
|
r.Header.Set(FrostfsNamespaceHeader, namespace)
|
|
chiRouter.ServeHTTP(w, r)
|
|
resp = readResponse(t, w)
|
|
require.Equal(t, s3middleware.PutObjectOperation, resp.Method)
|
|
|
|
// check we cannot access 'bucket' in custom namespace
|
|
w, r = httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, target, nil)
|
|
r.Header.Set(FrostfsNamespaceHeader, namespace)
|
|
chiRouter.ServeHTTP(w, r)
|
|
assertAPIError(t, w, apiErrors.ErrAccessDenied)
|
|
}
|
|
|
|
func TestPolicyCheckerReqTypeDetermination(t *testing.T) {
|
|
chiRouter := prepareRouter(t)
|
|
bktName, objName := "bucket", "object"
|
|
|
|
policy := engineiam.Policy{
|
|
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)
|
|
|
|
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)
|
|
bktName, objName := "bucket", "object"
|
|
target := fmt.Sprintf("/%s/%s", bktName, objName)
|
|
|
|
// check we can access bucket if rules not found
|
|
w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, target, nil)
|
|
chiRouter.ServeHTTP(w, r)
|
|
resp := readResponse(t, w)
|
|
require.Equal(t, s3middleware.PutObjectOperation, resp.Method)
|
|
|
|
// check we cannot access if rules not found when settings is enabled
|
|
chiRouter.middlewareSettings.denyByDefault = true
|
|
w, r = httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, target, nil)
|
|
chiRouter.ServeHTTP(w, r)
|
|
assertAPIError(t, w, apiErrors.ErrAccessDenied)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|