From b279bdffe46137a5341b26f1d03cc0f087a4f317 Mon Sep 17 00:00:00 2001
From: Marina Biryukova <m.biryukova@yadro.com>
Date: Tue, 16 Apr 2024 11:20:35 +0300
Subject: [PATCH] [#367] Add check of AccessBox attributes

Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
---
 api/auth/center.go               |  7 ++--
 api/auth/presign_test.go         |  7 ++--
 api/cache/accessbox.go           | 13 ++++---
 api/cache/cache_test.go          |  5 ++-
 api/handler/acl_test.go          |  8 ++---
 api/handler/cors_test.go         |  4 +--
 api/handler/handlers_test.go     |  2 +-
 api/handler/head_test.go         |  2 +-
 api/handler/put_test.go          | 22 ++++++------
 api/layer/listing.go             |  2 +-
 api/layer/versioning_test.go     |  4 +--
 api/middleware/auth.go           |  8 ++---
 api/middleware/policy.go         |  7 ++++
 api/middleware/util.go           | 60 +++++++++++++++++---------------
 api/router_mock_test.go          |  7 ++--
 api/router_test.go               | 24 +++++++++++++
 authmate/authmate.go             |  4 +--
 creds/tokens/credentials.go      | 50 +++++++++++++-------------
 creds/tokens/credentials_test.go | 14 +++++---
 go.mod                           |  2 +-
 go.sum                           |  4 +--
 internal/frostfs/authmate.go     | 11 +++---
 22 files changed, 157 insertions(+), 110 deletions(-)

diff --git a/api/auth/center.go b/api/auth/center.go
index 338088f..72fb7fe 100644
--- a/api/auth/center.go
+++ b/api/auth/center.go
@@ -186,7 +186,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
 		return nil, err
 	}
 
-	box, err := c.cli.GetBox(r.Context(), addr)
+	box, attrs, err := c.cli.GetBox(r.Context(), addr)
 	if err != nil {
 		return nil, fmt.Errorf("get box '%s': %w", addr, err)
 	}
@@ -207,6 +207,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
 			Region:      authHdr.Region,
 			SignatureV4: authHdr.SignatureV4,
 		},
+		Attributes: attrs,
 	}
 	if needClientTime {
 		result.ClientTime = signatureDateTime
@@ -274,7 +275,7 @@ func (c *Center) checkFormData(r *http.Request) (*middleware.Box, error) {
 		return nil, err
 	}
 
-	box, err := c.cli.GetBox(r.Context(), addr)
+	box, attrs, err := c.cli.GetBox(r.Context(), addr)
 	if err != nil {
 		return nil, fmt.Errorf("get box '%s': %w", addr, err)
 	}
@@ -289,7 +290,7 @@ func (c *Center) checkFormData(r *http.Request) (*middleware.Box, error) {
 			reqSignature, signature)
 	}
 
-	return &middleware.Box{AccessBox: box}, nil
+	return &middleware.Box{AccessBox: box, Attributes: attrs}, nil
 }
 
 func cloneRequest(r *http.Request, authHeader *AuthHeader) *http.Request {
diff --git a/api/auth/presign_test.go b/api/auth/presign_test.go
index 9cebb81..9e5d330 100644
--- a/api/auth/presign_test.go
+++ b/api/auth/presign_test.go
@@ -10,6 +10,7 @@ import (
 	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
 	apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
 	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
+	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
 	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
 	"github.com/aws/aws-sdk-go/aws/credentials"
 	"github.com/stretchr/testify/require"
@@ -31,13 +32,13 @@ func (m credentialsMock) addBox(addr oid.Address, box *accessbox.Box) {
 	m.boxes[addr.String()] = box
 }
 
-func (m credentialsMock) GetBox(_ context.Context, addr oid.Address) (*accessbox.Box, error) {
+func (m credentialsMock) GetBox(_ context.Context, addr oid.Address) (*accessbox.Box, []object.Attribute, error) {
 	box, ok := m.boxes[addr.String()]
 	if !ok {
-		return nil, &apistatus.ObjectNotFound{}
+		return nil, nil, &apistatus.ObjectNotFound{}
 	}
 
-	return box, nil
+	return box, nil, nil
 }
 
 func (m credentialsMock) Put(context.Context, cid.ID, tokens.CredentialsParam) (oid.Address, error) {
diff --git a/api/cache/accessbox.go b/api/cache/accessbox.go
index c8e3be9..9401804 100644
--- a/api/cache/accessbox.go
+++ b/api/cache/accessbox.go
@@ -6,6 +6,7 @@ import (
 
 	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
 	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
+	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
 	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
 	"github.com/bluele/gcache"
 	"go.uber.org/zap"
@@ -26,8 +27,9 @@ type (
 	}
 
 	AccessBoxCacheValue struct {
-		Box     *accessbox.Box
-		PutTime time.Time
+		Box        *accessbox.Box
+		Attributes []object.Attribute
+		PutTime    time.Time
 	}
 )
 
@@ -72,10 +74,11 @@ func (o *AccessBoxCache) Get(address oid.Address) *AccessBoxCacheValue {
 }
 
 // Put stores an accessbox to cache.
-func (o *AccessBoxCache) Put(address oid.Address, box *accessbox.Box) error {
+func (o *AccessBoxCache) Put(address oid.Address, box *accessbox.Box, attrs []object.Attribute) error {
 	val := &AccessBoxCacheValue{
-		Box:     box,
-		PutTime: time.Now(),
+		Box:        box,
+		Attributes: attrs,
+		PutTime:    time.Now(),
 	}
 	return o.cache.Set(address, val)
 }
diff --git a/api/cache/cache_test.go b/api/cache/cache_test.go
index 825c4e6..1cb05cc 100644
--- a/api/cache/cache_test.go
+++ b/api/cache/cache_test.go
@@ -6,6 +6,7 @@ import (
 	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
 	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
 	cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
+	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
 	oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/zap"
@@ -18,11 +19,13 @@ func TestAccessBoxCacheType(t *testing.T) {
 
 	addr := oidtest.Address()
 	box := &accessbox.Box{}
+	var attrs []object.Attribute
 
-	err := cache.Put(addr, box)
+	err := cache.Put(addr, box, attrs)
 	require.NoError(t, err)
 	val := cache.Get(addr)
 	require.Equal(t, box, val.Box)
+	require.Equal(t, attrs, val.Attributes)
 	require.Equal(t, 0, observedLog.Len())
 
 	err = cache.cache.Set(addr, "tmp")
diff --git a/api/handler/acl_test.go b/api/handler/acl_test.go
index f0c3b3f..838e682 100644
--- a/api/handler/acl_test.go
+++ b/api/handler/acl_test.go
@@ -1728,7 +1728,7 @@ func createBucketAssertS3Error(hc *handlerContext, bktName string, box *accessbo
 
 func createBucketBase(hc *handlerContext, bktName string, box *accessbox.Box) *httptest.ResponseRecorder {
 	w, r := prepareTestRequest(hc, bktName, "", nil)
-	ctx := middleware.SetBoxData(r.Context(), box)
+	ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
 	r = r.WithContext(ctx)
 	hc.Handler().CreateBucketHandler(w, r)
 	return w
@@ -1749,7 +1749,7 @@ func putBucketACLBase(hc *handlerContext, bktName string, box *accessbox.Box, he
 	for key, val := range header {
 		r.Header.Set(key, val)
 	}
-	ctx := middleware.SetBoxData(r.Context(), box)
+	ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
 	r = r.WithContext(ctx)
 	hc.Handler().PutBucketACLHandler(w, r)
 	return w
@@ -1779,7 +1779,7 @@ func putObjectACLBase(hc *handlerContext, bktName, objName string, box *accessbo
 	for key, val := range header {
 		r.Header.Set(key, val)
 	}
-	ctx := middleware.SetBoxData(r.Context(), box)
+	ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
 	r = r.WithContext(ctx)
 	hc.Handler().PutObjectACLHandler(w, r)
 	return w
@@ -1818,7 +1818,7 @@ func putObjectWithHeadersBase(hc *handlerContext, bktName, objName string, heade
 		r.Header.Set(k, v)
 	}
 
-	ctx := middleware.SetBoxData(r.Context(), box)
+	ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
 	r = r.WithContext(ctx)
 
 	hc.Handler().PutObjectHandler(w, r)
diff --git a/api/handler/cors_test.go b/api/handler/cors_test.go
index 0bfad16..1c4bd9e 100644
--- a/api/handler/cors_test.go
+++ b/api/handler/cors_test.go
@@ -23,14 +23,14 @@ func TestCORSOriginWildcard(t *testing.T) {
 	bktName := "bucket-for-cors"
 	box, _ := createAccessBox(t)
 	w, r := prepareTestRequest(hc, bktName, "", nil)
-	ctx := middleware.SetBoxData(r.Context(), box)
+	ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
 	r = r.WithContext(ctx)
 	r.Header.Add(api.AmzACL, "public-read")
 	hc.Handler().CreateBucketHandler(w, r)
 	assertStatus(t, w, http.StatusOK)
 
 	w, r = prepareTestPayloadRequest(hc, bktName, "", strings.NewReader(body))
-	ctx = middleware.SetBoxData(r.Context(), box)
+	ctx = middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
 	r = r.WithContext(ctx)
 	hc.Handler().PutBucketCorsHandler(w, r)
 	assertStatus(t, w, http.StatusOK)
diff --git a/api/handler/handlers_test.go b/api/handler/handlers_test.go
index 9b8f3f4..eb677d5 100644
--- a/api/handler/handlers_test.go
+++ b/api/handler/handlers_test.go
@@ -192,7 +192,7 @@ func prepareHandlerContextBase(t *testing.T, cacheCfg *layer.CachesConfig) *hand
 		h:       h,
 		tp:      tp,
 		tree:    treeMock,
-		context: middleware.SetBoxData(context.Background(), newTestAccessBox(t, key)),
+		context: middleware.SetBox(context.Background(), &middleware.Box{AccessBox: newTestAccessBox(t, key)}),
 		config:  cfg,
 
 		layerFeatures: features,
diff --git a/api/handler/head_test.go b/api/handler/head_test.go
index 821f651..fc68a17 100644
--- a/api/handler/head_test.go
+++ b/api/handler/head_test.go
@@ -94,7 +94,7 @@ func TestInvalidAccessThroughCache(t *testing.T) {
 	headObject(t, hc, bktName, objName, nil, http.StatusOK)
 
 	w, r := prepareTestRequest(hc, bktName, objName, nil)
-	hc.Handler().HeadObjectHandler(w, r.WithContext(middleware.SetBoxData(r.Context(), newTestAccessBox(t, nil))))
+	hc.Handler().HeadObjectHandler(w, r.WithContext(middleware.SetBox(r.Context(), &middleware.Box{AccessBox: newTestAccessBox(t, nil)})))
 	assertStatus(t, w, http.StatusForbidden)
 }
 
diff --git a/api/handler/put_test.go b/api/handler/put_test.go
index 889bdef..4ff00da 100644
--- a/api/handler/put_test.go
+++ b/api/handler/put_test.go
@@ -353,15 +353,17 @@ func getChunkedRequest(ctx context.Context, t *testing.T, bktName, objName strin
 	w := httptest.NewRecorder()
 	reqInfo := middleware.NewReqInfo(w, req, middleware.ObjectRequest{Bucket: bktName, Object: objName})
 	req = req.WithContext(middleware.SetReqInfo(ctx, reqInfo))
-	req = req.WithContext(middleware.SetClientTime(req.Context(), signTime))
-	req = req.WithContext(middleware.SetAuthHeaders(req.Context(), &middleware.AuthHeader{
-		AccessKeyID: AWSAccessKeyID,
-		SignatureV4: "4f232c4386841ef735655705268965c44a0e4690baa4adea153f7db9fa80a0a9",
-		Region:      "us-east-1",
-	}))
-	req = req.WithContext(middleware.SetBoxData(req.Context(), &accessbox.Box{
-		Gate: &accessbox.GateData{
-			SecretKey: AWSSecretAccessKey,
+	req = req.WithContext(middleware.SetBox(req.Context(), &middleware.Box{
+		ClientTime: signTime,
+		AuthHeaders: &middleware.AuthHeader{
+			AccessKeyID: AWSAccessKeyID,
+			SignatureV4: "4f232c4386841ef735655705268965c44a0e4690baa4adea153f7db9fa80a0a9",
+			Region:      "us-east-1",
+		},
+		AccessBox: &accessbox.Box{
+			Gate: &accessbox.GateData{
+				SecretKey: AWSSecretAccessKey,
+			},
 		},
 	}))
 
@@ -401,7 +403,7 @@ func TestCreateNamespacedBucket(t *testing.T) {
 
 	box, _ := createAccessBox(t)
 	w, r := prepareTestRequest(hc, bktName, "", nil)
-	ctx := middleware.SetBoxData(r.Context(), box)
+	ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
 	reqInfo := middleware.GetReqInfo(ctx)
 	reqInfo.Namespace = namespace
 	r = r.WithContext(middleware.SetReqInfo(ctx, reqInfo))
diff --git a/api/layer/listing.go b/api/layer/listing.go
index 2de4a21..cda75d1 100644
--- a/api/layer/listing.go
+++ b/api/layer/listing.go
@@ -334,7 +334,7 @@ func (n *layer) initNewVersionsByPrefixSession(ctx context.Context, p commonVers
 	session.Context, session.Cancel = context.WithCancel(context.Background())
 
 	if bd, err := middleware.GetBoxData(ctx); err == nil {
-		session.Context = middleware.SetBoxData(session.Context, bd)
+		session.Context = middleware.SetBox(session.Context, &middleware.Box{AccessBox: bd})
 	}
 
 	session.Stream, err = n.treeService.InitVersionsByPrefixStream(session.Context, p.BktInfo, p.Prefix, latestOnly)
diff --git a/api/layer/versioning_test.go b/api/layer/versioning_test.go
index 705ec00..2c5ca5f 100644
--- a/api/layer/versioning_test.go
+++ b/api/layer/versioning_test.go
@@ -145,12 +145,12 @@ func prepareContext(t *testing.T, cachesConfig ...*CachesConfig) *testContext {
 	bearerToken := bearertest.Token()
 	require.NoError(t, bearerToken.Sign(key.PrivateKey))
 
-	ctx := middleware.SetBoxData(context.Background(), &accessbox.Box{
+	ctx := middleware.SetBox(context.Background(), &middleware.Box{AccessBox: &accessbox.Box{
 		Gate: &accessbox.GateData{
 			BearerToken: &bearerToken,
 			GateKey:     key.PublicKey(),
 		},
-	})
+	}})
 	tp := NewTestFrostFS(key)
 
 	bktName := "testbucket1"
diff --git a/api/middleware/auth.go b/api/middleware/auth.go
index 950e3c7..c5318ca 100644
--- a/api/middleware/auth.go
+++ b/api/middleware/auth.go
@@ -13,6 +13,7 @@ import (
 	frostfsErrors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/errors"
 	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
 	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
+	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
 	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
 	"go.uber.org/zap"
 )
@@ -23,6 +24,7 @@ type (
 		AccessBox   *accessbox.Box
 		ClientTime  time.Time
 		AuthHeaders *AuthHeader
+		Attributes  []object.Attribute
 	}
 
 	// Center is a user authentication interface.
@@ -65,11 +67,7 @@ func Auth(center Center, log *zap.Logger) Func {
 					return
 				}
 			} else {
-				ctx = SetBoxData(ctx, box.AccessBox)
-				if !box.ClientTime.IsZero() {
-					ctx = SetClientTime(ctx, box.ClientTime)
-				}
-				ctx = SetAuthHeaders(ctx, box.AuthHeaders)
+				ctx = SetBox(ctx, box)
 
 				if box.AccessBox.Gate.BearerToken != nil {
 					reqInfo.User = bearer.ResolveIssuer(*box.AccessBox.Gate.BearerToken).String()
diff --git a/api/middleware/policy.go b/api/middleware/policy.go
index 48f1eb3..3af3fd5 100644
--- a/api/middleware/policy.go
+++ b/api/middleware/policy.go
@@ -444,6 +444,13 @@ func determineProperties(r *http.Request, decoder XMLDecoder, resolver BucketRes
 		res[k] = v
 	}
 
+	attrs, err := GetAccessBoxAttrs(r.Context())
+	if err == nil {
+		for _, attr := range attrs {
+			res[fmt.Sprintf(s3.PropertyKeyFormatAccessBoxAttr, attr.Key())] = attr.Value()
+		}
+	}
+
 	return res, nil
 }
 
diff --git a/api/middleware/util.go b/api/middleware/util.go
index 8bdd38a..dd62c55 100644
--- a/api/middleware/util.go
+++ b/api/middleware/util.go
@@ -6,29 +6,27 @@ import (
 	"time"
 
 	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
+	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
 )
 
 // keyWrapper is wrapper for context keys.
 type keyWrapper string
 
-// authHeaders is a wrapper for authentication headers of a request.
-var authHeadersKey = keyWrapper("__context_auth_headers_key")
-
-// boxData is an ID used to store accessbox.Box in a context.
-var boxDataKey = keyWrapper("__context_box_key")
-
-// clientTime is an ID used to store client time.Time in a context.
-var clientTimeKey = keyWrapper("__context_client_time")
+// boxKey is an ID used to store Box in a context.
+var boxKey = keyWrapper("__context_box_key")
 
 // GetBoxData  extracts accessbox.Box from context.
 func GetBoxData(ctx context.Context) (*accessbox.Box, error) {
-	var box *accessbox.Box
-	data, ok := ctx.Value(boxDataKey).(*accessbox.Box)
+	data, ok := ctx.Value(boxKey).(*Box)
 	if !ok || data == nil {
+		return nil, fmt.Errorf("couldn't get box from context")
+	}
+
+	if data.AccessBox == nil {
 		return nil, fmt.Errorf("couldn't get box data from context")
 	}
 
-	box = data
+	box := data.AccessBox
 	if box.Gate == nil {
 		box.Gate = &accessbox.GateData{}
 	}
@@ -37,35 +35,39 @@ func GetBoxData(ctx context.Context) (*accessbox.Box, error) {
 
 // GetAuthHeaders extracts auth.AuthHeader from context.
 func GetAuthHeaders(ctx context.Context) (*AuthHeader, error) {
-	authHeaders, ok := ctx.Value(authHeadersKey).(*AuthHeader)
-	if !ok {
-		return nil, fmt.Errorf("couldn't get auth headers from context")
+	data, ok := ctx.Value(boxKey).(*Box)
+	if !ok || data == nil {
+		return nil, fmt.Errorf("couldn't get box from context")
 	}
 
-	return authHeaders, nil
+	return data.AuthHeaders, nil
 }
 
 // GetClientTime extracts time.Time from context.
 func GetClientTime(ctx context.Context) (time.Time, error) {
-	clientTime, ok := ctx.Value(clientTimeKey).(time.Time)
-	if !ok {
+	data, ok := ctx.Value(boxKey).(*Box)
+	if !ok || data == nil {
+		return time.Time{}, fmt.Errorf("couldn't get box from context")
+	}
+
+	if data.ClientTime.IsZero() {
 		return time.Time{}, fmt.Errorf("couldn't get client time from context")
 	}
 
-	return clientTime, nil
+	return data.ClientTime, nil
 }
 
-// SetBoxData sets accessbox.Box in the context.
-func SetBoxData(ctx context.Context, box *accessbox.Box) context.Context {
-	return context.WithValue(ctx, boxDataKey, box)
+// GetAccessBoxAttrs extracts []object.Attribute from context.
+func GetAccessBoxAttrs(ctx context.Context) ([]object.Attribute, error) {
+	data, ok := ctx.Value(boxKey).(*Box)
+	if !ok || data == nil {
+		return nil, fmt.Errorf("couldn't get box from context")
+	}
+
+	return data.Attributes, nil
 }
 
-// SetAuthHeaders sets auth.AuthHeader in the context.
-func SetAuthHeaders(ctx context.Context, header *AuthHeader) context.Context {
-	return context.WithValue(ctx, authHeadersKey, header)
-}
-
-// SetClientTime sets time.Time in the context.
-func SetClientTime(ctx context.Context, newTime time.Time) context.Context {
-	return context.WithValue(ctx, clientTimeKey, newTime)
+// SetBox sets Box in the context.
+func SetBox(ctx context.Context, box *Box) context.Context {
+	return context.WithValue(ctx, boxKey, box)
 }
diff --git a/api/router_mock_test.go b/api/router_mock_test.go
index 8747b1a..49ff865 100644
--- a/api/router_mock_test.go
+++ b/api/router_mock_test.go
@@ -15,6 +15,7 @@ import (
 	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
 	bearertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer/test"
 	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
+	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
 	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
 	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
 	"github.com/nspcc-dev/neo-go/pkg/util"
@@ -31,8 +32,9 @@ func (p *poolStatisticMock) Statistic() pool.Statistic {
 }
 
 type centerMock struct {
-	t    *testing.T
-	anon bool
+	t     *testing.T
+	anon  bool
+	attrs []object.Attribute
 }
 
 func (c *centerMock) Authenticate(*http.Request) (*middleware.Box, error) {
@@ -53,6 +55,7 @@ func (c *centerMock) Authenticate(*http.Request) (*middleware.Box, error) {
 				BearerToken: token,
 			},
 		},
+		Attributes: c.attrs,
 	}, nil
 }
 
diff --git a/api/router_test.go b/api/router_test.go
index a401196..fdeb851 100644
--- a/api/router_test.go
+++ b/api/router_test.go
@@ -17,6 +17,7 @@ import (
 	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"
@@ -576,6 +577,29 @@ func TestResourceTagsCheck(t *testing.T) {
 	})
 }
 
+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 allowOperations(router *routerMock, ns string, operations []string, conditions engineiam.Conditions) {
 	addPolicy(router, ns, "allow", engineiam.AllowEffect, operations, conditions)
 }
diff --git a/authmate/authmate.go b/authmate/authmate.go
index 5a64279..f124a30 100644
--- a/authmate/authmate.go
+++ b/authmate/authmate.go
@@ -344,7 +344,7 @@ func (a *Agent) UpdateSecret(ctx context.Context, w io.Writer, options *UpdateSe
 
 	creds := tokens.New(cfg)
 
-	box, err := creds.GetBox(ctx, options.Address)
+	box, _, err := creds.GetBox(ctx, options.Address)
 	if err != nil {
 		return fmt.Errorf("get accessbox: %w", err)
 	}
@@ -431,7 +431,7 @@ func (a *Agent) ObtainSecret(ctx context.Context, w io.Writer, options *ObtainSe
 		return fmt.Errorf("failed to parse secret address: %w", err)
 	}
 
-	box, err := bearerCreds.GetBox(ctx, addr)
+	box, _, err := bearerCreds.GetBox(ctx, addr)
 	if err != nil {
 		return fmt.Errorf("failed to get tokens: %w", err)
 	}
diff --git a/creds/tokens/credentials.go b/creds/tokens/credentials.go
index 24abb4b..f926536 100644
--- a/creds/tokens/credentials.go
+++ b/creds/tokens/credentials.go
@@ -22,7 +22,7 @@ import (
 type (
 	// Credentials is a bearer token get/put interface.
 	Credentials interface {
-		GetBox(context.Context, oid.Address) (*accessbox.Box, error)
+		GetBox(context.Context, oid.Address) (*accessbox.Box, []object.Attribute, error)
 		Put(context.Context, cid.ID, CredentialsParam) (oid.Address, error)
 		Update(context.Context, oid.Address, CredentialsParam) (oid.Address, error)
 	}
@@ -86,13 +86,13 @@ type FrostFS interface {
 	// prevented the object from being created.
 	CreateObject(context.Context, PrmObjectCreate) (oid.ID, error)
 
-	// GetCredsPayload gets payload of the credential object from FrostFS network.
+	// GetCredsObject gets the credential object from FrostFS network.
 	// It uses search by system name and select using CRDT 2PSet. In case of absence CRDT header
 	// it heads object by address.
 	//
 	// It returns exactly one non-nil value. It returns any error encountered which
 	// prevented the object payload from being read.
-	GetCredsPayload(context.Context, oid.Address) ([]byte, error)
+	GetCredsObject(context.Context, oid.Address) (*object.Object, error)
 }
 
 var (
@@ -115,72 +115,72 @@ func New(cfg Config) Credentials {
 	}
 }
 
-func (c *cred) GetBox(ctx context.Context, addr oid.Address) (*accessbox.Box, error) {
+func (c *cred) GetBox(ctx context.Context, addr oid.Address) (*accessbox.Box, []object.Attribute, error) {
 	cachedBoxValue := c.cache.Get(addr)
 	if cachedBoxValue != nil {
 		return c.checkIfCredentialsAreRemoved(ctx, addr, cachedBoxValue)
 	}
 
-	box, err := c.getAccessBox(ctx, addr)
+	box, attrs, err := c.getAccessBox(ctx, addr)
 	if err != nil {
-		return nil, fmt.Errorf("get access box: %w", err)
+		return nil, nil, fmt.Errorf("get access box: %w", err)
 	}
 
 	cachedBox, err := box.GetBox(c.key)
 	if err != nil {
-		return nil, fmt.Errorf("get gate box: %w", err)
+		return nil, nil, fmt.Errorf("get gate box: %w", err)
 	}
 
-	c.putBoxToCache(addr, cachedBox)
+	c.putBoxToCache(addr, cachedBox, attrs)
 
-	return cachedBox, nil
+	return cachedBox, attrs, nil
 }
 
-func (c *cred) checkIfCredentialsAreRemoved(ctx context.Context, addr oid.Address, cachedBoxValue *cache.AccessBoxCacheValue) (*accessbox.Box, error) {
+func (c *cred) checkIfCredentialsAreRemoved(ctx context.Context, addr oid.Address, cachedBoxValue *cache.AccessBoxCacheValue) (*accessbox.Box, []object.Attribute, error) {
 	if time.Since(cachedBoxValue.PutTime) < c.removingCheckDuration {
-		return cachedBoxValue.Box, nil
+		return cachedBoxValue.Box, cachedBoxValue.Attributes, nil
 	}
 
-	box, err := c.getAccessBox(ctx, addr)
+	box, attrs, err := c.getAccessBox(ctx, addr)
 	if err != nil {
 		if client.IsErrObjectAlreadyRemoved(err) {
 			c.cache.Delete(addr)
-			return nil, fmt.Errorf("get access box: %w", err)
+			return nil, nil, fmt.Errorf("get access box: %w", err)
 		}
-		return cachedBoxValue.Box, nil
+		return cachedBoxValue.Box, cachedBoxValue.Attributes, nil
 	}
 
 	cachedBox, err := box.GetBox(c.key)
 	if err != nil {
 		c.cache.Delete(addr)
-		return nil, fmt.Errorf("get gate box: %w", err)
+		return nil, nil, fmt.Errorf("get gate box: %w", err)
 	}
 	// we need this to reset PutTime
 	// to don't check for removing each time after removingCheckDuration interval
-	c.putBoxToCache(addr, cachedBox)
+	c.putBoxToCache(addr, cachedBox, attrs)
 
-	return cachedBoxValue.Box, nil
+	return cachedBoxValue.Box, attrs, nil
 }
 
-func (c *cred) putBoxToCache(addr oid.Address, box *accessbox.Box) {
-	if err := c.cache.Put(addr, box); err != nil {
+func (c *cred) putBoxToCache(addr oid.Address, box *accessbox.Box, attrs []object.Attribute) {
+	if err := c.cache.Put(addr, box, attrs); err != nil {
 		c.log.Warn(logs.CouldntPutAccessBoxIntoCache, zap.String("address", addr.EncodeToString()))
 	}
 }
 
-func (c *cred) getAccessBox(ctx context.Context, addr oid.Address) (*accessbox.AccessBox, error) {
-	data, err := c.frostFS.GetCredsPayload(ctx, addr)
+func (c *cred) getAccessBox(ctx context.Context, addr oid.Address) (*accessbox.AccessBox, []object.Attribute, error) {
+	obj, err := c.frostFS.GetCredsObject(ctx, addr)
 	if err != nil {
-		return nil, fmt.Errorf("read payload: %w", err)
+		return nil, nil, fmt.Errorf("read payload and attributes: %w", err)
 	}
 
 	// decode access box
 	var box accessbox.AccessBox
-	if err = box.Unmarshal(data); err != nil {
-		return nil, fmt.Errorf("unmarhal access box: %w", err)
+	if err = box.Unmarshal(obj.Payload()); err != nil {
+		return nil, nil, fmt.Errorf("unmarhal access box: %w", err)
 	}
 
-	return &box, nil
+	return &box, obj.Attributes(), nil
 }
 
 func (c *cred) Put(ctx context.Context, idCnr cid.ID, prm CredentialsParam) (oid.Address, error) {
diff --git a/creds/tokens/credentials_test.go b/creds/tokens/credentials_test.go
index dfd3c56..b38a465 100644
--- a/creds/tokens/credentials_test.go
+++ b/creds/tokens/credentials_test.go
@@ -11,6 +11,7 @@ import (
 	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
 	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
 	apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
+	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
 	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
 	oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
 	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
@@ -27,7 +28,7 @@ func (f *frostfsMock) CreateObject(context.Context, PrmObjectCreate) (oid.ID, er
 	panic("implement me for test")
 }
 
-func (f *frostfsMock) GetCredsPayload(_ context.Context, address oid.Address) ([]byte, error) {
+func (f *frostfsMock) GetCredsObject(_ context.Context, address oid.Address) (*object.Object, error) {
 	if err := f.errors[address]; err != nil {
 		return nil, err
 	}
@@ -36,7 +37,10 @@ func (f *frostfsMock) GetCredsPayload(_ context.Context, address oid.Address) ([
 	if !ok {
 		return nil, errors.New("not found")
 	}
-	return data, nil
+
+	var obj object.Object
+	obj.SetPayload(data)
+	return &obj, nil
 }
 
 func TestRemovingAccessBox(t *testing.T) {
@@ -78,14 +82,14 @@ func TestRemovingAccessBox(t *testing.T) {
 
 	creds := New(cfg)
 
-	_, err = creds.GetBox(ctx, addr)
+	_, _, err = creds.GetBox(ctx, addr)
 	require.NoError(t, err)
 
 	frostfs.errors[addr] = errors.New("network error")
-	_, err = creds.GetBox(ctx, addr)
+	_, _, err = creds.GetBox(ctx, addr)
 	require.NoError(t, err)
 
 	frostfs.errors[addr] = &apistatus.ObjectAlreadyRemoved{}
-	_, err = creds.GetBox(ctx, addr)
+	_, _, err = creds.GetBox(ctx, addr)
 	require.Error(t, err)
 }
diff --git a/go.mod b/go.mod
index 9a36895..b17e58c 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240409115729-6eb492025bdd
 	git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20230531082742-c97d21411eb6
 	git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240402141549-3790142b10c7
-	git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240412102212-530248de754c
+	git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240416071728-04a79f57ef1f
 	git.frostfs.info/TrueCloudLab/zapjournald v0.0.0-20240124114243-cb2e66427d02
 	github.com/aws/aws-sdk-go v1.44.6
 	github.com/bluele/gcache v0.0.2
diff --git a/go.sum b/go.sum
index e64ba5b..042a80e 100644
--- a/go.sum
+++ b/go.sum
@@ -48,8 +48,8 @@ git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240402141549-3790142b10c7
 git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240402141549-3790142b10c7/go.mod h1:i0RKqiF4z3UOxLSNwhHw+cUz/JyYWuTRpnn9ere4Y3w=
 git.frostfs.info/TrueCloudLab/hrw v1.2.1 h1:ccBRK21rFvY5R1WotI6LNoPlizk7qSvdfD8lNIRudVc=
 git.frostfs.info/TrueCloudLab/hrw v1.2.1/go.mod h1:C1Ygde2n843yTZEQ0FP69jYiuaYV0kriLvP4zm8JuvM=
-git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240412102212-530248de754c h1:Ei15WKKLDXoFqEIe292szb3RfsyLqRZfeJX2FjFvz6k=
-git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240412102212-530248de754c/go.mod h1:H/AW85RtYxVTbcgwHW76DqXeKlsiCIOeNXHPqyDBrfQ=
+git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240416071728-04a79f57ef1f h1:hP1Q/MJvRsHSBIWXn48C+hVsRHfPWWLhdOg6IxjaWBs=
+git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240416071728-04a79f57ef1f/go.mod h1:H/AW85RtYxVTbcgwHW76DqXeKlsiCIOeNXHPqyDBrfQ=
 git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 h1:M2KR3iBj7WpY3hP10IevfIB9MURr4O9mwVfJ+SjT3HA=
 git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0/go.mod h1:okpbKfVYf/BpejtfFTfhZqFP+sZ8rsHrP8Rr/jYPNRc=
 git.frostfs.info/TrueCloudLab/tzhash v1.8.0 h1:UFMnUIk0Zh17m8rjGHJMqku2hCgaXDqjqZzS4gsb4UA=
diff --git a/internal/frostfs/authmate.go b/internal/frostfs/authmate.go
index 42bade3..9de2263 100644
--- a/internal/frostfs/authmate.go
+++ b/internal/frostfs/authmate.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"context"
 	"fmt"
-	"io"
 	"strconv"
 	"time"
 
@@ -15,6 +14,7 @@ import (
 	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/crdt"
 	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
 	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
+	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
 	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
 	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
 	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
@@ -69,8 +69,8 @@ func (x *AuthmateFrostFS) CreateContainer(ctx context.Context, prm authmate.PrmC
 	return res.ContainerID, nil
 }
 
-// GetCredsPayload implements authmate.FrostFS interface method.
-func (x *AuthmateFrostFS) GetCredsPayload(ctx context.Context, addr oid.Address) ([]byte, error) {
+// GetCredsObject implements authmate.FrostFS interface method.
+func (x *AuthmateFrostFS) GetCredsObject(ctx context.Context, addr oid.Address) (*object.Object, error) {
 	versions, err := x.getCredVersions(ctx, addr)
 	if err != nil {
 		return nil, err
@@ -85,14 +85,13 @@ func (x *AuthmateFrostFS) GetCredsPayload(ctx context.Context, addr oid.Address)
 		Container:   addr.Container(),
 		Object:      credObjID,
 		WithPayload: true,
+		WithHeader:  true,
 	})
 	if err != nil {
 		return nil, err
 	}
 
-	defer res.Payload.Close()
-
-	return io.ReadAll(res.Payload)
+	return res.Head, err
 }
 
 // CreateObject implements authmate.FrostFS interface method.