[#306] policy: Change default access strategy

Use access strategy based on bucket type and/or config flags.

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
pull/310/head
Denis Kirillov 2024-02-27 12:45:37 +03:00
parent 1bfea006b0
commit 3285a2e105
4 changed files with 334 additions and 98 deletions

View File

@ -1,6 +1,7 @@
package middleware
import (
"context"
"crypto/elliptic"
"fmt"
"net/http"
@ -19,27 +20,29 @@ import (
)
type PolicySettings interface {
ResolveNamespaceAlias(ns string) string
PolicyDenyByDefault() bool
ACLEnabled() bool
}
type FrostFSIDInformer interface {
GetUserGroupIDs(userHash util.Uint160) ([]string, error)
}
func PolicyCheck(storage engine.ChainRouter, frostfsid FrostFSIDInformer, settings PolicySettings, domains []string, log *zap.Logger) Func {
type PolicyConfig struct {
Storage engine.ChainRouter
FrostfsID FrostFSIDInformer
Settings PolicySettings
Domains []string
Log *zap.Logger
BucketResolver BucketResolveFunc
}
func PolicyCheck(cfg PolicyConfig) Func {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
st, err := policyCheck(storage, frostfsid, settings, domains, r)
if err == nil {
if st != chain.Allow && (st != chain.NoRuleFound || settings.PolicyDenyByDefault()) {
err = apiErr.GetAPIErrorWithError(apiErr.ErrAccessDenied, fmt.Errorf("policy check: %s", st.String()))
}
}
if err != nil {
reqLogOrDefault(ctx, log).Error(logs.PolicyValidationFailed, zap.Error(err))
if err := policyCheck(r, cfg); err != nil {
reqLogOrDefault(ctx, cfg.Log).Error(logs.PolicyValidationFailed, zap.Error(err))
WriteErrorResponse(w, GetReqInfo(ctx), err)
return
}
@ -49,27 +52,58 @@ func PolicyCheck(storage engine.ChainRouter, frostfsid FrostFSIDInformer, settin
}
}
func policyCheck(storage engine.ChainRouter, frostfsid FrostFSIDInformer, settings PolicySettings, domains []string, r *http.Request) (chain.Status, error) {
req, err := getPolicyRequest(r, frostfsid, domains)
func policyCheck(r *http.Request, cfg PolicyConfig) error {
reqType, bktName, objName := getBucketObject(r, cfg.Domains)
req, err := getPolicyRequest(r, cfg.FrostfsID, reqType, bktName, objName)
if err != nil {
return 0, err
return err
}
reqInfo := GetReqInfo(r.Context())
target := engine.NewRequestTargetWithNamespace(settings.ResolveNamespaceAlias(reqInfo.Namespace))
st, found, err := storage.IsAllowed(chain.S3, target, req)
target := engine.NewRequestTargetWithNamespace(reqInfo.Namespace)
st, found, err := cfg.Storage.IsAllowed(chain.S3, target, req)
if err != nil {
return 0, err
return err
}
if !found {
st = chain.NoRuleFound
}
return st, nil
switch {
case st == chain.Allow:
return nil
case st != chain.NoRuleFound:
return apiErr.GetAPIErrorWithError(apiErr.ErrAccessDenied, fmt.Errorf("policy check: %s", st.String()))
}
isAPE, err := isAPEBehavior(r.Context(), req, cfg, reqType, bktName)
if err != nil {
return err
}
if isAPE && cfg.Settings.PolicyDenyByDefault() {
return apiErr.GetAPIErrorWithError(apiErr.ErrAccessDenied, fmt.Errorf("policy check: %s", st.String()))
}
return nil
}
func getPolicyRequest(r *http.Request, frostfsid FrostFSIDInformer, domains []string) (*testutil.Request, error) {
func isAPEBehavior(ctx context.Context, req *testutil.Request, cfg PolicyConfig, reqType ReqType, bktName string) (bool, error) {
if reqType == noneType ||
strings.HasSuffix(req.Operation(), CreateBucketOperation) {
return !cfg.Settings.ACLEnabled(), nil
}
bktInfo, err := cfg.BucketResolver(ctx, bktName) // we cannot use reqInfo.BucketName because it hasn't been set yet
if err != nil {
return false, err
}
return bktInfo.APEEnabled, nil
}
func getPolicyRequest(r *http.Request, frostfsid FrostFSIDInformer, reqType ReqType, bktName string, objName string) (*testutil.Request, error) {
var (
owner string
groups []string
@ -90,7 +124,14 @@ func getPolicyRequest(r *http.Request, frostfsid FrostFSIDInformer, domains []st
}
}
op, res := determineOperationAndResource(r, domains)
op := determineOperation(r, reqType)
var res string
switch reqType {
case objectType:
res = fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName, objName)
default:
res = fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName)
}
return testutil.NewRequest(op, testutil.NewResource(res, nil),
map[string]string{
@ -108,45 +149,34 @@ const (
objectType
)
func determineOperationAndResource(r *http.Request, domains []string) (operation string, resource string) {
var (
reqType ReqType
matchDomain bool
)
func getBucketObject(r *http.Request, domains []string) (reqType ReqType, bktName string, objName string) {
for _, domain := range domains {
ind := strings.Index(r.Host, "."+domain)
if ind == -1 {
continue
}
matchDomain = true
reqType = bucketType
bkt := r.Host[:ind]
if obj := strings.TrimPrefix(r.URL.Path, "/"); obj != "" {
reqType = objectType
resource = fmt.Sprintf(s3.ResourceFormatS3BucketObject, bkt, obj)
} else {
resource = fmt.Sprintf(s3.ResourceFormatS3Bucket, bkt)
return objectType, bkt, obj
}
break
return bucketType, bkt, ""
}
if !matchDomain {
bktObj := strings.TrimPrefix(r.URL.Path, "/")
if ind := strings.IndexByte(bktObj, '/'); ind == -1 {
reqType = bucketType
resource = fmt.Sprintf(s3.ResourceFormatS3Bucket, bktObj)
if bktObj == "" {
reqType = noneType
}
} else {
reqType = objectType
resource = fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktObj[:ind], bktObj[ind+1:])
}
bktObj := strings.TrimPrefix(r.URL.Path, "/")
if bktObj == "" {
return noneType, "", ""
}
if ind := strings.IndexByte(bktObj, '/'); ind != -1 {
return objectType, bktObj[:ind], bktObj[ind+1:]
}
return bucketType, bktObj, ""
}
func determineOperation(r *http.Request, reqType ReqType) (operation string) {
switch reqType {
case objectType:
operation = determineObjectOperation(r)
@ -156,7 +186,7 @@ func determineOperationAndResource(r *http.Request, domains []string) (operation
operation = determineGeneralOperation(r)
}
return "s3:" + operation, resource
return "s3:" + operation
}
func determineBucketOperation(r *http.Request) string {

View File

@ -136,7 +136,14 @@ func NewRouter(cfg Config) *chi.Mux {
api.Use(s3middleware.FrostfsIDValidation(cfg.FrostfsID, cfg.Log))
}
api.Use(s3middleware.PolicyCheck(cfg.PolicyChecker, cfg.FrostfsID, cfg.MiddlewareSettings, cfg.Domains, cfg.Log))
api.Use(s3middleware.PolicyCheck(s3middleware.PolicyConfig{
Storage: cfg.PolicyChecker,
FrostfsID: cfg.FrostfsID,
Settings: cfg.MiddlewareSettings,
Domains: cfg.Domains,
Log: cfg.Log,
BucketResolver: cfg.Handler.ResolveBucket,
}))
defaultRouter := chi.NewRouter()
defaultRouter.Mount(fmt.Sprintf("/{%s}", s3middleware.BucketURLPrm), bucketRouter(cfg.Handler, cfg.Log))

View File

@ -3,6 +3,7 @@ package api
import (
"context"
"encoding/json"
"errors"
"net/http"
"testing"
@ -53,6 +54,7 @@ func (c *centerMock) Authenticate(*http.Request) (*middleware.Box, error) {
type middlewareSettingsMock struct {
denyByDefault bool
aclEnabled bool
}
func (r *middlewareSettingsMock) NamespaceHeader() string {
@ -67,6 +69,10 @@ func (r *middlewareSettingsMock) PolicyDenyByDefault() bool {
return r.denyByDefault
}
func (r *middlewareSettingsMock) ACLEnabled() bool {
return r.aclEnabled
}
type frostFSIDMock struct {
}
@ -79,7 +85,9 @@ func (f *frostFSIDMock) GetUserGroupIDs(util.Uint160) ([]string, error) {
}
type handlerMock struct {
t *testing.T
t *testing.T
cfg *middlewareSettingsMock
buckets map[string]*data.BucketInfo
}
type handlerResult struct {
@ -339,9 +347,20 @@ func (h *handlerMock) PutBucketNotificationHandler(http.ResponseWriter, *http.Re
panic("implement me")
}
func (h *handlerMock) CreateBucketHandler(http.ResponseWriter, *http.Request) {
//TODO implement me
panic("implement me")
func (h *handlerMock) CreateBucketHandler(w http.ResponseWriter, r *http.Request) {
reqInfo := middleware.GetReqInfo(r.Context())
h.buckets[reqInfo.Namespace+reqInfo.BucketName] = &data.BucketInfo{
Name: reqInfo.BucketName,
APEEnabled: !h.cfg.ACLEnabled(),
}
res := &handlerResult{
Method: middleware.CreateBucketOperation,
ReqInfo: middleware.GetReqInfo(r.Context()),
}
h.writeResponse(w, res)
}
func (h *handlerMock) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
@ -443,8 +462,13 @@ func (h *handlerMock) ListMultipartUploadsHandler(w http.ResponseWriter, r *http
h.writeResponse(w, res)
}
func (h *handlerMock) ResolveBucket(context.Context, string) (*data.BucketInfo, error) {
return &data.BucketInfo{}, nil
func (h *handlerMock) ResolveBucket(ctx context.Context, name string) (*data.BucketInfo, error) {
reqInfo := middleware.GetReqInfo(ctx)
bktInfo, ok := h.buckets[reqInfo.Namespace+name]
if !ok {
return nil, errors.New("not found")
}
return bktInfo, nil
}
func (h *handlerMock) writeResponse(w http.ResponseWriter, resp *handlerResult) {

View File

@ -11,6 +11,7 @@ import (
"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"
@ -27,6 +28,7 @@ import (
)
type routerMock struct {
t *testing.T
router *chi.Mux
cfg Config
middlewareSettings *middlewareSettingsMock
@ -55,7 +57,7 @@ func prepareRouter(t *testing.T) *routerMock {
Limit: 10,
BacklogTimeout: 30 * time.Second,
},
Handler: &handlerMock{t: t},
Handler: &handlerMock{t: t, cfg: middlewareSettings, buckets: map[string]*data.BucketInfo{}},
Center: &centerMock{t: t},
Log: logger,
Metrics: metrics.NewAppMetrics(metricsConfig),
@ -65,6 +67,7 @@ func prepareRouter(t *testing.T) *routerMock {
FrostfsID: &frostFSIDMock{},
}
return &routerMock{
t: t,
router: NewRouter(cfg),
cfg: cfg,
middlewareSettings: middlewareSettings,
@ -75,6 +78,8 @@ func prepareRouter(t *testing.T) *routerMock {
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)
@ -90,6 +95,8 @@ func TestRouterUploadPart(t *testing.T) {
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)
@ -104,22 +111,18 @@ func TestRouterListMultipartUploads(t *testing.T) {
func TestRouterObjectWithSlashes(t *testing.T) {
chiRouter := prepareRouter(t)
bktName, objName := "dkirillov", "/fix/object"
target := fmt.Sprintf("/%s/%s", bktName, objName)
ns, bktName, objName := "", "dkirillov", "/fix/object"
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPut, target, nil)
chiRouter.ServeHTTP(w, r)
resp := readResponse(t, w)
require.Equal(t, "PutObject", resp.Method)
createBucket(chiRouter, ns, bktName)
resp := putObject(chiRouter, ns, bktName, objName)
require.Equal(t, objName, resp.ReqInfo.ObjectName)
}
func TestRouterObjectEscaping(t *testing.T) {
chiRouter := prepareRouter(t)
bktName := "dkirillov"
ns, bktName := "", "dkirillov"
createBucket(chiRouter, ns, bktName)
for _, tc := range []struct {
name string
@ -153,14 +156,7 @@ func TestRouterObjectEscaping(t *testing.T) {
},
} {
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)
resp := putObject(chiRouter, ns, bktName, tc.objName)
require.Equal(t, tc.expectedObjName, resp.ReqInfo.ObjectName)
})
}
@ -168,40 +164,33 @@ func TestRouterObjectEscaping(t *testing.T) {
func TestPolicyChecker(t *testing.T) {
chiRouter := prepareRouter(t)
namespace := "custom-ns"
bktName, objName := "bucket", "object"
target := fmt.Sprintf("/%s/%s", bktName, objName)
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, bktName)}},
Resources: chain.Resources{Names: []string{fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName1)}},
}},
}
_, _, err := chiRouter.policyChecker.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, engine.NamespaceTarget(namespace), ruleChain)
_, _, err := chiRouter.policyChecker.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, engine.NamespaceTarget(ns2), 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)
putObject(chiRouter, ns1, bktName1, objName1)
// 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)
putObject(chiRouter, ns2, bktName2, objName2)
// 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)
putObjectErr(chiRouter, ns2, bktName1, objName2, apiErrors.ErrAccessDenied)
}
func TestPolicyCheckerReqTypeDetermination(t *testing.T) {
@ -248,20 +237,206 @@ func TestPolicyCheckerReqTypeDetermination(t *testing.T) {
func TestDefaultBehaviorPolicyChecker(t *testing.T) {
chiRouter := prepareRouter(t)
bktName, objName := "bucket", "object"
target := fmt.Sprintf("/%s/%s", bktName, objName)
ns, bktName := "", "bucket"
// 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)
createBucket(chiRouter, ns, bktName)
// 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)
createBucketErr(chiRouter, ns, bktName, apiErrors.ErrAccessDenied)
}
func TestACLAPE(t *testing.T) {
t.Run("acl disabled, ape deny by default", func(t *testing.T) {
router := prepareRouter(t)
ns, bktName, objName := "", "bucket", "object"
bktNameOld, bktNameNew := "old-bucket", "new-bucket"
createOldBucket(router, bktNameOld)
createNewBucket(router, bktNameNew)
router.middlewareSettings.aclEnabled = false
router.middlewareSettings.denyByDefault = true
// Allow because of using old bucket
putObject(router, ns, bktNameOld, objName)
// Deny because of deny by default
putObjectErr(router, ns, bktNameNew, objName, apiErrors.ErrAccessDenied)
// Deny because of deny by default
createBucketErr(router, ns, bktName, apiErrors.ErrAccessDenied)
listBucketsErr(router, ns, apiErrors.ErrAccessDenied)
// Allow operations and check
allowOperations(router, ns, []string{"s3:CreateBucket", "s3:ListBuckets"})
createBucket(router, ns, bktName)
listBuckets(router, ns)
})
t.Run("acl disabled, ape allow by default", func(t *testing.T) {
router := prepareRouter(t)
ns, bktName, objName := "", "bucket", "object"
bktNameOld, bktNameNew := "old-bucket", "new-bucket"
createOldBucket(router, bktNameOld)
createNewBucket(router, bktNameNew)
router.middlewareSettings.aclEnabled = false
router.middlewareSettings.denyByDefault = false
// Allow because of using old bucket
putObject(router, ns, bktNameOld, objName)
// Allow because of allow by default
putObject(router, ns, bktNameNew, objName)
// Allow because of deny by default
createBucket(router, ns, bktName)
listBuckets(router, ns)
// Deny operations and check
denyOperations(router, ns, []string{"s3:CreateBucket", "s3:ListBuckets"})
createBucketErr(router, ns, bktName, apiErrors.ErrAccessDenied)
listBucketsErr(router, ns, apiErrors.ErrAccessDenied)
})
t.Run("acl enabled, ape deny by default", func(t *testing.T) {
router := prepareRouter(t)
ns, bktName, objName := "", "bucket", "object"
bktNameOld, bktNameNew := "old-bucket", "new-bucket"
createOldBucket(router, bktNameOld)
createNewBucket(router, bktNameNew)
router.middlewareSettings.aclEnabled = true
router.middlewareSettings.denyByDefault = true
// Allow because of using old bucket
putObject(router, ns, bktNameOld, objName)
// Deny because of deny by default
putObjectErr(router, ns, bktNameNew, objName, apiErrors.ErrAccessDenied)
// Allow because of old behavior
createBucket(router, ns, bktName)
listBuckets(router, ns)
})
t.Run("acl enabled, ape allow by default", func(t *testing.T) {
router := prepareRouter(t)
ns, bktName, objName := "", "bucket", "object"
bktNameOld, bktNameNew := "old-bucket", "new-bucket"
createOldBucket(router, bktNameOld)
createNewBucket(router, bktNameNew)
router.middlewareSettings.aclEnabled = true
router.middlewareSettings.denyByDefault = false
// Allow because of using old bucket
putObject(router, ns, bktNameOld, objName)
// Allow because of allow by default
putObject(router, ns, bktNameNew, objName)
// Allow because of old behavior
createBucket(router, ns, bktName)
listBuckets(router, ns)
})
}
func allowOperations(router *routerMock, ns string, operations []string) {
addPolicy(router, ns, "allow", engineiam.AllowEffect, operations)
}
func denyOperations(router *routerMock, ns string, operations []string) {
addPolicy(router, ns, "deny", engineiam.DenyEffect, operations)
}
func addPolicy(router *routerMock, ns string, id string, effect engineiam.Effect, operations []string) {
policy := engineiam.Policy{
Statement: []engineiam.Statement{{
Principal: map[engineiam.PrincipalType][]string{engineiam.Wildcard: {}},
Effect: effect,
Action: engineiam.Action(operations),
Resource: engineiam.Resource{fmt.Sprintf(s3.ResourceFormatS3All)},
}},
}
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 createOldBucket(router *routerMock, bktName string) {
createSpecificBucket(router, bktName, true)
}
func createNewBucket(router *routerMock, bktName string) {
createSpecificBucket(router, bktName, false)
}
func createSpecificBucket(router *routerMock, bktName string, old bool) {
aclEnabled := router.middlewareSettings.ACLEnabled()
router.middlewareSettings.aclEnabled = old
createBucket(router, "", bktName)
router.middlewareSettings.aclEnabled = aclEnabled
}
func createBucket(router *routerMock, namespace, bktName string) {
w := createBucketBase(router, namespace, bktName)
resp := readResponse(router.t, w)
require.Equal(router.t, s3middleware.CreateBucketOperation, resp.Method)
}
func createBucketErr(router *routerMock, namespace, bktName string, errCode apiErrors.ErrorCode) {
w := createBucketBase(router, namespace, bktName)
assertAPIError(router.t, w, errCode)
}
func createBucketBase(router *routerMock, namespace, bktName string) *httptest.ResponseRecorder {
w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, "/"+bktName, nil)
r.Header.Set(FrostfsNamespaceHeader, namespace)
router.ServeHTTP(w, r)
return w
}
func listBuckets(router *routerMock, namespace string) {
w := listBucketsBase(router, namespace)
resp := readResponse(router.t, w)
require.Equal(router.t, s3middleware.ListBucketsOperation, resp.Method)
}
func listBucketsErr(router *routerMock, namespace string, errCode apiErrors.ErrorCode) {
w := listBucketsBase(router, namespace)
assertAPIError(router.t, w, errCode)
}
func listBucketsBase(router *routerMock, namespace string) *httptest.ResponseRecorder {
w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil)
r.Header.Set(FrostfsNamespaceHeader, namespace)
router.ServeHTTP(w, r)
return w
}
func putObject(router *routerMock, namespace, bktName, objName string) handlerResult {
w := putObjectBase(router, namespace, bktName, objName)
resp := readResponse(router.t, w)
require.Equal(router.t, s3middleware.PutObjectOperation, resp.Method)
return resp
}
func putObjectErr(router *routerMock, namespace, bktName, objName string, errCode apiErrors.ErrorCode) {
w := putObjectBase(router, namespace, bktName, objName)
assertAPIError(router.t, w, errCode)
}
func putObjectBase(router *routerMock, namespace, bktName, objName string) *httptest.ResponseRecorder {
w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, "/"+bktName+"/"+objName, nil)
r.Header.Set(FrostfsNamespaceHeader, namespace)
router.ServeHTTP(w, r)
return w
}
func TestOwnerIDRetrieving(t *testing.T) {