diff --git a/api/middleware/policy.go b/api/middleware/policy.go index da69a0e5..bb647833 100644 --- a/api/middleware/policy.go +++ b/api/middleware/policy.go @@ -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 { diff --git a/api/router.go b/api/router.go index 1e7cf80e..398f773d 100644 --- a/api/router.go +++ b/api/router.go @@ -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)) diff --git a/api/router_mock_test.go b/api/router_mock_test.go index 7f62997a..f29dcf2e 100644 --- a/api/router_mock_test.go +++ b/api/router_mock_test.go @@ -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) { diff --git a/api/router_test.go b/api/router_test.go index 8b2b7209..59543111 100644 --- a/api/router_test.go +++ b/api/router_test.go @@ -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: ¢erMock{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) {