From 5b2edd2a4635145e422334ce9ff98e32c8cf5dc6 Mon Sep 17 00:00:00 2001
From: Pavel Pogodaev
Date: Tue, 21 Jan 2025 15:08:34 +0300
Subject: [PATCH] [#604] Add support of MFADelete argument and x-amz-mfa header
Signed-off-by: Pavel Pogodaev
---
api/cache/cache_test.go | 5 +-
api/data/info.go | 22 +-
api/errors/errors.go | 9 +-
api/handler/api.go | 7 +-
api/handler/copy_test.go | 4 +-
api/handler/delete.go | 58 +++-
api/handler/delete_test.go | 45 ++-
api/handler/handlers_test.go | 73 ++++-
api/handler/multipart_upload_test.go | 4 +-
api/handler/object_list_test.go | 8 +-
api/handler/put.go | 11 +-
api/handler/util.go | 14 +
api/handler/versioning.go | 49 ++-
api/headers.go | 1 +
api/layer/locking_test.go | 7 +-
api/layer/system_object.go | 5 +-
api/layer/versioning_test.go | 22 +-
cmd/s3-gw/app.go | 47 ++-
cmd/s3-gw/app_settings.go | 1 +
config/config.env | 1 +
config/config.yaml | 1 +
docs/configuration.md | 2 +
go.mod | 51 ++--
go.sum | 113 ++++---
internal/frostfs/mfa.go | 440 +++++++++++++++++++++++++++
internal/logs/logs.go | 8 +
pkg/service/tree/tree.go | 20 +-
pkg/service/tree/tree_test.go | 5 +-
28 files changed, 912 insertions(+), 121 deletions(-)
create mode 100644 internal/frostfs/mfa.go
diff --git a/api/cache/cache_test.go b/api/cache/cache_test.go
index 36230d93..70e128a0 100644
--- a/api/cache/cache_test.go
+++ b/api/cache/cache_test.go
@@ -173,7 +173,10 @@ func TestSettingsCacheType(t *testing.T) {
cache := NewSystemCache(DefaultSystemConfig(logger))
key := "key"
- settings := &data.BucketSettings{Versioning: data.VersioningEnabled}
+ settings := &data.BucketSettings{Versioning: data.Versioning{
+ VersioningStatus: data.VersioningEnabled,
+ MFADeleteStatus: data.MFADeleteEnabled,
+ }}
err := cache.PutSettings(key, settings)
require.NoError(t, err)
diff --git a/api/data/info.go b/api/data/info.go
index c9f9df69..f5584b2c 100644
--- a/api/data/info.go
+++ b/api/data/info.go
@@ -23,6 +23,9 @@ const (
VersioningSuspended = "Suspended"
corsFilePathTemplate = "/%s.cors"
+
+ MFADeleteDisabled = "Disabled"
+ MFADeleteEnabled = "Enabled"
)
type (
@@ -58,12 +61,19 @@ type (
// BucketSettings stores settings such as versioning.
BucketSettings struct {
- Versioning string
+ Versioning Versioning
LockConfiguration *ObjectLockConfiguration
CannedACL string
OwnerKey *keys.PublicKey
}
+ // Versioning stores bucket versioning settings.
+ Versioning struct {
+ VersioningStatus string
+ MFADeleteStatus string
+ MFASerialNumber string
+ }
+
// CORSConfiguration stores CORS configuration of a request.
CORSConfiguration struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CORSConfiguration" json:"-"`
@@ -138,15 +148,19 @@ func (o *ObjectInfo) ETag(md5Enabled bool) string {
}
func (b BucketSettings) Unversioned() bool {
- return b.Versioning == VersioningUnversioned
+ return b.Versioning.VersioningStatus == VersioningUnversioned
}
func (b BucketSettings) VersioningEnabled() bool {
- return b.Versioning == VersioningEnabled
+ return b.Versioning.VersioningStatus == VersioningEnabled
}
func (b BucketSettings) VersioningSuspended() bool {
- return b.Versioning == VersioningSuspended
+ return b.Versioning.VersioningStatus == VersioningSuspended
+}
+
+func (b BucketSettings) MFADeleteEnabled() bool {
+ return b.Versioning.MFADeleteStatus == MFADeleteEnabled
}
func Quote(val string) string {
diff --git a/api/errors/errors.go b/api/errors/errors.go
index 27ac5669..b7e49c6c 100644
--- a/api/errors/errors.go
+++ b/api/errors/errors.go
@@ -142,6 +142,7 @@ const (
ErrInvalidTagDirective
// Add new error codes here.
ErrNotSupported
+ ErrMFAAuthNeeded
// SSE-S3 related API errors.
ErrInvalidEncryptionMethod
@@ -286,7 +287,7 @@ const (
ErrPostPolicyConditionInvalidFormat
- //CORS configuration errors.
+ // CORS configuration errors.
ErrCORSUnsupportedMethod
ErrCORSWildcardExposeHeaders
@@ -387,6 +388,12 @@ var errorCodes = errorCodeMap{
Description: "Access Denied.",
HTTPStatusCode: http.StatusForbidden,
},
+ ErrMFAAuthNeeded: {
+ ErrCode: ErrMFAAuthNeeded,
+ Code: "AccessDenied",
+ Description: "Mfa Authentication must be used for this request",
+ HTTPStatusCode: http.StatusForbidden,
+ },
ErrAccessControlListNotSupported: {
ErrCode: ErrAccessControlListNotSupported,
Code: "AccessControlListNotSupported",
diff --git a/api/handler/api.go b/api/handler/api.go
index fa5c57b8..db36b359 100644
--- a/api/handler/api.go
+++ b/api/handler/api.go
@@ -9,6 +9,7 @@ import (
"strings"
"time"
+ "git.frostfs.info/TrueCloudLab/frostfs-mfa/mfa"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
@@ -24,6 +25,7 @@ type (
cfg Config
ape APE
frostfsid FrostFSID
+ mfa *mfa.Manager
}
// Config contains data which handler needs to keep.
@@ -69,7 +71,7 @@ const (
var _ api.Handler = (*handler)(nil)
// New creates new api.Handler using given logger and client.
-func New(log *zap.Logger, obj *layer.Layer, cfg Config, storage APE, ffsid FrostFSID) (api.Handler, error) {
+func New(log *zap.Logger, obj *layer.Layer, cfg Config, storage APE, ffsid FrostFSID, mfaMgr *mfa.Manager) (api.Handler, error) {
switch {
case obj == nil:
return nil, errors.New("empty FrostFS Object Layer")
@@ -79,6 +81,8 @@ func New(log *zap.Logger, obj *layer.Layer, cfg Config, storage APE, ffsid Frost
return nil, errors.New("empty policy storage")
case ffsid == nil:
return nil, errors.New("empty frostfsid")
+ case mfaMgr == nil:
+ return nil, errors.New("empty mfaMgr")
}
return &handler{
@@ -87,6 +91,7 @@ func New(log *zap.Logger, obj *layer.Layer, cfg Config, storage APE, ffsid Frost
cfg: cfg,
ape: storage,
frostfsid: ffsid,
+ mfa: mfaMgr,
}, nil
}
diff --git a/api/handler/copy_test.go b/api/handler/copy_test.go
index 6c25b795..0c28159d 100644
--- a/api/handler/copy_test.go
+++ b/api/handler/copy_test.go
@@ -64,11 +64,11 @@ func TestCopyToItself(t *testing.T) {
copyObject(tc, bktName, objName, objName, CopyMeta{}, http.StatusBadRequest)
copyObject(tc, bktName, objName, objName, copyMeta, http.StatusOK)
- putBucketVersioning(t, tc, bktName, true)
+ putBucketVersioning(t, tc, bktName, true, "")
copyObject(tc, bktName, objName, objName, CopyMeta{}, http.StatusOK)
copyObject(tc, bktName, objName, objName, copyMeta, http.StatusOK)
- putBucketVersioning(t, tc, bktName, false)
+ putBucketVersioning(t, tc, bktName, false, "")
copyObject(tc, bktName, objName, objName, CopyMeta{}, http.StatusOK)
copyObject(tc, bktName, objName, objName, copyMeta, http.StatusOK)
}
diff --git a/api/handler/delete.go b/api/handler/delete.go
index 5c08e9fc..454317ec 100644
--- a/api/handler/delete.go
+++ b/api/handler/delete.go
@@ -15,6 +15,7 @@ import (
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
+ "github.com/pquerna/otp/totp"
)
// limitation of AWS https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html
@@ -84,6 +85,26 @@ func (h *handler) DeleteObjectHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ if len(versionID) > 0 && bktSettings.MFADeleteEnabled() {
+ serialNumber, token, err := h.getMFAHeader(r)
+ if err != nil {
+ h.logAndSendError(ctx, w, "could not get mfa header", reqInfo, err)
+ return
+ }
+
+ device, err := h.mfa.GetMFADevice(ctx, reqInfo.Namespace, nameFromArn(serialNumber))
+ if err != nil {
+ h.logAndSendError(ctx, w, "could not get mfa device", reqInfo, err)
+ return
+ }
+
+ validate := totp.Validate(token, device.Key.Secret())
+ if !validate {
+ h.logAndSendError(ctx, w, "could not validate token", reqInfo, fmt.Errorf("mfa Authentication must be used for this request"))
+ return
+ }
+ }
+
networkInfo, err := h.obj.GetNetworkInfo(ctx)
if err != nil {
h.logAndSendError(ctx, w, "could not get network info", reqInfo, err)
@@ -186,6 +207,26 @@ func (h *handler) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Re
return
}
+ if haveVersionedObjects(requested.Objects) && bktSettings.MFADeleteEnabled() {
+ serialNumber, token, err := h.getMFAHeader(r)
+ if err != nil {
+ h.logAndSendError(ctx, w, "could not get mfa header", reqInfo, err)
+ return
+ }
+
+ device, err := h.mfa.GetMFADevice(ctx, reqInfo.Namespace, nameFromArn(serialNumber))
+ if err != nil {
+ h.logAndSendError(ctx, w, "could not get mfa device", reqInfo, err)
+ return
+ }
+
+ validate := totp.Validate(token, device.Key.Secret())
+ if !validate {
+ h.logAndSendError(ctx, w, "could not validate token", reqInfo, fmt.Errorf("mfa Authentication must be used for this request"))
+ return
+ }
+ }
+
networkInfo, err := h.obj.GetNetworkInfo(ctx)
if err != nil {
h.logAndSendError(ctx, w, "could not get network info", reqInfo, err)
@@ -245,6 +286,12 @@ func (h *handler) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ bktSettings, err := h.obj.GetBucketSettings(ctx, bktInfo)
+ if err != nil {
+ h.logAndSendError(ctx, w, "could not get bucket settings", reqInfo, err)
+ return
+ }
+
if err = checkOwner(bktInfo, reqInfo.User); err != nil {
h.logAndSendError(ctx, w, "request owner id does not match bucket owner id", reqInfo, err)
return
@@ -260,7 +307,7 @@ func (h *handler) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
skipObjCheck := false
if value, ok := r.Header[api.AmzForceBucketDelete]; ok {
s := value[0]
- if s == "true" {
+ if s == "true" && !bktSettings.MFADeleteEnabled() {
skipObjCheck = true
}
}
@@ -287,3 +334,12 @@ func (h *handler) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
+
+func haveVersionedObjects(objects []ObjectIdentifier) bool {
+ for _, o := range objects {
+ if len(o.VersionID) > 0 {
+ return true
+ }
+ }
+ return false
+}
diff --git a/api/handler/delete_test.go b/api/handler/delete_test.go
index ed0c992b..bf55e464 100644
--- a/api/handler/delete_test.go
+++ b/api/handler/delete_test.go
@@ -123,7 +123,7 @@ func TestDeleteObjectsError(t *testing.T) {
bktName, objName := "bucket-for-removal", "object-to-delete"
bktInfo := createTestBucket(hc, bktName)
- putBucketVersioning(t, hc, bktName, true)
+ putBucketVersioning(t, hc, bktName, true, "")
putObject(hc, bktName, objName)
@@ -301,7 +301,7 @@ func TestDeleteMarkerSuspended(t *testing.T) {
bktName, objName := "bucket-for-removal", "object-to-delete"
bktInfo, _ := createVersionedBucketAndObject(t, tc, bktName, objName)
- putBucketVersioning(t, tc, bktName, false)
+ putBucketVersioning(t, tc, bktName, false, "")
t.Run("not create new delete marker if last version is delete marker", func(t *testing.T) {
deleteMarkerVersion, isDeleteMarker := deleteObject(t, tc, bktName, objName, emptyVersion)
@@ -349,7 +349,7 @@ func TestDeleteObjectCombined(t *testing.T) {
bktName, objName := "bucket-for-removal", "object-to-delete"
bktInfo, objInfo := createBucketAndObject(tc, bktName, objName)
- putBucketVersioning(t, tc, bktName, true)
+ putBucketVersioning(t, tc, bktName, true, "")
checkFound(t, tc, bktName, objName, emptyVersion)
deleteObject(t, tc, bktName, objName, emptyVersion)
@@ -366,13 +366,13 @@ func TestDeleteObjectSuspended(t *testing.T) {
bktName, objName := "bucket-for-removal", "object-to-delete"
bktInfo, objInfo := createBucketAndObject(tc, bktName, objName)
- putBucketVersioning(t, tc, bktName, true)
+ putBucketVersioning(t, tc, bktName, true, "")
checkFound(t, tc, bktName, objName, emptyVersion)
deleteObject(t, tc, bktName, objName, emptyVersion)
checkNotFound(t, tc, bktName, objName, emptyVersion)
- putBucketVersioning(t, tc, bktName, false)
+ putBucketVersioning(t, tc, bktName, false, "")
deleteObject(t, tc, bktName, objName, emptyVersion)
checkNotFound(t, tc, bktName, objName, objInfo.VersionID())
@@ -385,7 +385,7 @@ func TestDeleteMarkers(t *testing.T) {
bktName, objName := "bucket-for-removal", "object-to-delete"
createTestBucket(tc, bktName)
- putBucketVersioning(t, tc, bktName, true)
+ putBucketVersioning(t, tc, bktName, true, "")
checkNotFound(t, tc, bktName, objName, emptyVersion)
deleteObject(t, tc, bktName, objName, emptyVersion)
@@ -404,7 +404,7 @@ func TestGetHeadDeleteMarker(t *testing.T) {
bktName, objName := "bucket-for-removal", "object-to-delete"
createTestBucket(hc, bktName)
- putBucketVersioning(t, hc, bktName, true)
+ putBucketVersioning(t, hc, bktName, true, "")
putObject(hc, bktName, objName)
@@ -471,6 +471,16 @@ func TestDeleteBucketByNotOwner(t *testing.T) {
deleteBucket(t, hc, bktName, http.StatusNoContent)
}
+func TestDeleteObjectMFAEnabled(t *testing.T) {
+ hc := prepareHandlerContext(t)
+
+ bktName := "bucket-name"
+ deviceName := "mfa"
+ serialNumber := "arn:aws:iam:::mfa/" + deviceName
+ token := "123456"
+ _ = createVersionedBucketMFAEnabled(hc, bktName, serialNumber+" "+token)
+}
+
func createBucketAndObject(tc *handlerContext, bktName, objName string) (*data.BucketInfo, *data.ObjectInfo) {
bktInfo := createTestBucket(tc, bktName)
@@ -488,17 +498,32 @@ func createVersionedBucketAndObject(_ *testing.T, tc *handlerContext, bktName, o
func createVersionedBucket(hc *handlerContext, bktName string) *data.BucketInfo {
bktInfo := createTestBucket(hc, bktName)
- putBucketVersioning(hc.t, hc, bktName, true)
+ putBucketVersioning(hc.t, hc, bktName, true, "")
return bktInfo
}
-func putBucketVersioning(t *testing.T, tc *handlerContext, bktName string, enabled bool) {
+func createVersionedBucketMFAEnabled(hc *handlerContext, bktName, mfa string) *data.BucketInfo {
+ bktInfo := createTestBucket(hc, bktName)
+ putBucketVersioning(hc.t, hc, bktName, true, mfa)
+
+ return bktInfo
+}
+
+func putBucketVersioning(t *testing.T, tc *handlerContext, bktName string, enabled bool, mfa string) {
cfg := &VersioningConfiguration{Status: "Suspended"}
if enabled {
cfg.Status = "Enabled"
}
+ if len(mfa) > 0 {
+ cfg.MfaDelete = "Enabled"
+ }
w, r := prepareTestRequest(tc, bktName, "", cfg)
+
+ if len(mfa) > 0 {
+ r.Header.Set(api.AmzMFA, mfa)
+ }
+
tc.Handler().PutBucketVersioningHandler(w, r)
assertStatus(t, w, http.StatusOK)
}
@@ -622,6 +647,6 @@ func createSuspendedBucket(t *testing.T, tc *handlerContext, bktName string) *da
createTestBucket(tc, bktName)
bktInfo, err := tc.Layer().GetBucketInfo(tc.Context(), bktName)
require.NoError(t, err)
- putBucketVersioning(t, tc, bktName, false)
+ putBucketVersioning(t, tc, bktName, false, "")
return bktInfo
}
diff --git a/api/handler/handlers_test.go b/api/handler/handlers_test.go
index 0e66df5d..550dd501 100644
--- a/api/handler/handlers_test.go
+++ b/api/handler/handlers_test.go
@@ -16,6 +16,7 @@ import (
"testing"
"time"
+ "git.frostfs.info/TrueCloudLab/frostfs-mfa/mfa"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
@@ -267,6 +268,7 @@ func prepareHandlerContextBase(config *handlerConfig, log *zap.Logger) (*handler
cfg: cfg,
ape: newAPEMock(),
frostfsid: newFrostfsIDMock(),
+ mfa: newMFAMock(),
}
accessBox, err := newTestAccessBox(key)
@@ -461,6 +463,72 @@ func newFrostfsIDMock() *frostfsidMock {
return &frostfsidMock{data: map[string]*keys.PublicKey{}}
}
+func newMFAMock() *mfa.Manager {
+ cfg := mfa.Config{
+ Storage: newStorageMock(),
+ Unlocker: nil,
+ Container: cid.ID{},
+ Logger: nil,
+ }
+
+ manager, _ := mfa.NewManager(cfg)
+
+ return man
+}
+
+type man mfa.Manager
+
+func (m man) GetMFADevice(ctx context.Context, ns, mfaName string) (*mfa.SecretDevice, error) {
+ // TODO implement me
+ panic("implement me")
+}
+
+type mfaOperations interface {
+ GetMFADevice(ctx context.Context, ns, mfaName string) (*mfa.SecretDevice, error)
+}
+
+type storage struct {
+}
+
+func newStorageMock() *storage {
+ return &storage{}
+}
+
+func (s *storage) CreateObject(_ context.Context, _ mfa.PrmObjectCreate) (oid.ID, error) {
+ // TODO implement me
+ panic("implement me")
+}
+
+func (s *storage) GetObject(_ context.Context, _ oid.Address) ([]byte, error) {
+ // TODO implement me
+ panic("implement me")
+}
+
+func (s *storage) DeleteObject(_ context.Context, _ oid.Address) error {
+ // TODO implement me
+ panic("implement me")
+}
+
+func (s *storage) SetTreeNode(_ context.Context, _ cid.ID, _ string, _ map[string]string) (*mfa.TreeMultiNode, error) {
+ // TODO implement me
+ panic("implement me")
+}
+
+func (s *storage) GetTreeNode(_ context.Context, _ cid.ID, _ string) (*mfa.TreeMultiNode, error) {
+ // TODO implement me
+ panic("implement me")
+}
+
+func (s *storage) DeleteTreeNode(_ context.Context, _ cid.ID, _ string) ([]*mfa.TreeNode, error) {
+ // TODO implement me
+ panic("implement me")
+}
+
+func (s *storage) GetTreeNodes(_ context.Context, _ cid.ID, _ string) ([]*mfa.TreeNode, error) {
+ // TODO implement me
+ panic("implement me")
+}
+
func (f *frostfsidMock) GetUserAddress(account, user string) (string, error) {
res, ok := f.data[account+user]
if !ok {
@@ -510,7 +578,10 @@ func createTestBucketWithLock(hc *handlerContext, bktName string, conf *data.Obj
sp := &layer.PutSettingsParams{
BktInfo: bktInfo,
Settings: &data.BucketSettings{
- Versioning: data.VersioningEnabled,
+ Versioning: data.Versioning{
+ VersioningStatus: data.VersioningEnabled,
+ MFADeleteStatus: data.MFADeleteDisabled,
+ },
LockConfiguration: conf,
OwnerKey: key.PublicKey(),
},
diff --git a/api/handler/multipart_upload_test.go b/api/handler/multipart_upload_test.go
index 576a4e7c..4f3c7ccf 100644
--- a/api/handler/multipart_upload_test.go
+++ b/api/handler/multipart_upload_test.go
@@ -69,7 +69,7 @@ func TestDeleteMultipartAllParts(t *testing.T) {
// versions bucket
createTestBucket(hc, bktName2)
- putBucketVersioning(t, hc, bktName2, true)
+ putBucketVersioning(t, hc, bktName2, true, "")
multipartUpload(hc, bktName2, objName, nil, objLen, partSize)
_, hdr := getObject(hc, bktName2, objName)
versionID := hdr.Get("X-Amz-Version-Id")
@@ -107,7 +107,7 @@ func TestSpecialMultipartName(t *testing.T) {
bktName, objName := "bucket", "bucket-settings"
createTestBucket(hc, bktName)
- putBucketVersioning(t, hc, bktName, true)
+ putBucketVersioning(t, hc, bktName, true, "")
createMultipartUpload(hc, bktName, objName, nil)
res := getBucketVersioning(hc, bktName)
diff --git a/api/handler/object_list_test.go b/api/handler/object_list_test.go
index 0dbb5b8c..c68d2e32 100644
--- a/api/handler/object_list_test.go
+++ b/api/handler/object_list_test.go
@@ -64,7 +64,7 @@ func TestListObjectNullVersions(t *testing.T) {
createTestBucket(hc, bktName)
putObjectContent(hc, bktName, objName, "content")
- putBucketVersioning(t, hc, bktName, true)
+ putBucketVersioning(t, hc, bktName, true, "")
putObjectContent(hc, bktName, objName, "content2")
result := listVersions(t, hc, bktName)
@@ -226,7 +226,7 @@ func TestListObjectsLatestVersions(t *testing.T) {
bktName := "bucket-versioning-enabled"
createTestBucket(hc, bktName)
- putBucketVersioning(t, hc, bktName, true)
+ putBucketVersioning(t, hc, bktName, true, "")
objName1, objName2 := "object1", "object2"
objContent1, objContent2 := "content1", "content2"
@@ -762,7 +762,7 @@ func TestMintVersioningListObjectVersionsVersionIDContinuation(t *testing.T) {
bktName, objName := "mint-bucket-for-listing-versions", "objName"
createTestBucket(hc, bktName)
- putBucketVersioning(t, hc, bktName, true)
+ putBucketVersioning(t, hc, bktName, true, "")
length := 10
objects := make([]string, length)
@@ -795,7 +795,7 @@ func TestListObjectVersionsEncoding(t *testing.T) {
bktName := "bucket-for-listing-versions-encoding"
bktInfo := createTestBucket(hc, bktName)
- putBucketVersioning(t, hc, bktName, true)
+ putBucketVersioning(t, hc, bktName, true, "")
objects := []string{"foo()/bar", "foo()/bar/xyzzy", "auux ab/thud", "asdf+b"}
for _, objName := range objects {
diff --git a/api/handler/put.go b/api/handler/put.go
index b70b05ca..76e6938c 100644
--- a/api/handler/put.go
+++ b/api/handler/put.go
@@ -862,14 +862,17 @@ func (h *handler) createBucketHandlerPolicy(w http.ResponseWriter, r *http.Reque
sp := &layer.PutSettingsParams{
BktInfo: bktInfo,
Settings: &data.BucketSettings{
- CannedACL: cannedACL,
- OwnerKey: key,
- Versioning: data.VersioningUnversioned,
+ CannedACL: cannedACL,
+ OwnerKey: key,
+ Versioning: data.Versioning{
+ VersioningStatus: data.VersioningUnversioned,
+ MFADeleteStatus: data.MFADeleteDisabled,
+ },
},
}
if p.ObjectLockEnabled {
- sp.Settings.Versioning = data.VersioningEnabled
+ sp.Settings.Versioning.VersioningStatus = data.VersioningEnabled
}
err = retryer.MakeWithRetry(ctx, func() error {
diff --git a/api/handler/util.go b/api/handler/util.go
index 0e86dd67..602fe683 100644
--- a/api/handler/util.go
+++ b/api/handler/util.go
@@ -168,3 +168,17 @@ func parseRange(s string) (*layer.RangeParams, error) {
End: values[1],
}, nil
}
+
+func nameFromArn(arn string) string {
+ pts := strings.Split(arn, "/")
+ return pts[len(pts)-1]
+}
+
+func (h *handler) getMFAHeader(r *http.Request) (string, string, error) {
+ parts := strings.Split(r.Header.Get(api.AmzMFA), " ")
+ if len(parts) != 2 {
+ return "", "", fmt.Errorf("%w: invalid mfa header", apierr.GetAPIError(apierr.ErrMFAAuthNeeded))
+ }
+
+ return parts[0], parts[1], nil
+}
diff --git a/api/handler/versioning.go b/api/handler/versioning.go
index 5a3c0eae..65bf1455 100644
--- a/api/handler/versioning.go
+++ b/api/handler/versioning.go
@@ -1,6 +1,7 @@
package handler
import (
+ "fmt"
"net/http"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
@@ -8,6 +9,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
+ "github.com/pquerna/otp/totp"
)
func (h *handler) PutBucketVersioningHandler(w http.ResponseWriter, r *http.Request) {
@@ -15,6 +17,7 @@ func (h *handler) PutBucketVersioningHandler(w http.ResponseWriter, r *http.Requ
defer span.End()
reqInfo := middleware.GetReqInfo(ctx)
+ var serialNumber, token string
configuration := new(VersioningConfiguration)
if err := h.cfg.NewXMLDecoder(r.Body, r.UserAgent()).Decode(configuration); err != nil {
@@ -34,14 +37,49 @@ func (h *handler) PutBucketVersioningHandler(w http.ResponseWriter, r *http.Requ
return
}
+ newMfa := len(configuration.MfaDelete) > 0
+
+ if settings.MFADeleteEnabled() || newMfa {
+ serialNumber, token, err = h.getMFAHeader(r)
+ if err != nil {
+ h.logAndSendError(ctx, w, "invalid x-amz-mfa header", reqInfo, errors.GetAPIError(errors.ErrBadRequest))
+ return
+ }
+ device, err := h.mfa.GetMFADevice(ctx, reqInfo.Namespace, nameFromArn(serialNumber))
+ if err != nil {
+ h.logAndSendError(ctx, w, "could not get mfa device", reqInfo, err)
+ return
+ }
+
+ validate := totp.Validate(token, device.Key.Secret())
+ if !validate {
+ h.logAndSendError(ctx, w, "could not validate token", reqInfo, fmt.Errorf("mfa Authentication must be used for this request"))
+ return
+ }
+ }
+
+ // settings pointer is stored in the cache, so modify a copy of the settings
+ newSettings := *settings
+
+ if newMfa {
+ switch configuration.MfaDelete {
+ case data.MFADeleteEnabled:
+ newSettings.Versioning.MFADeleteStatus = data.MFADeleteEnabled
+ newSettings.Versioning.MFASerialNumber = serialNumber
+ case data.MFADeleteDisabled:
+ newSettings.Versioning.MFADeleteStatus = data.MFADeleteDisabled
+ default:
+ h.logAndSendError(ctx, w, "failed to get mfa configuration", reqInfo, nil)
+ return
+ }
+ }
+
if configuration.Status != data.VersioningEnabled && configuration.Status != data.VersioningSuspended {
h.logAndSendError(ctx, w, "invalid versioning configuration", reqInfo, errors.GetAPIError(errors.ErrMalformedXML))
return
}
- // settings pointer is stored in the cache, so modify a copy of the settings
- newSettings := *settings
- newSettings.Versioning = configuration.Status
+ newSettings.Versioning.VersioningStatus = configuration.Status
p := &layer.PutSettingsParams{
BktInfo: bktInfo,
@@ -85,7 +123,10 @@ func (h *handler) GetBucketVersioningHandler(w http.ResponseWriter, r *http.Requ
func formVersioningConfiguration(settings *data.BucketSettings) *VersioningConfiguration {
res := &VersioningConfiguration{}
if !settings.Unversioned() {
- res.Status = settings.Versioning
+ res.Status = settings.Versioning.VersioningStatus
+ }
+ if settings.MFADeleteEnabled() {
+ res.MfaDelete = data.MFADeleteEnabled
}
return res
diff --git a/api/headers.go b/api/headers.go
index 2316b77e..de1fe3d4 100644
--- a/api/headers.go
+++ b/api/headers.go
@@ -15,6 +15,7 @@ const (
AmzCopySource = "X-Amz-Copy-Source"
AmzCopySourceRange = "X-Amz-Copy-Source-Range"
AmzDate = "X-Amz-Date"
+ AmzMFA = "X-Amz-Mfa"
LastModified = "Last-Modified"
Date = "Date"
diff --git a/api/layer/locking_test.go b/api/layer/locking_test.go
index 56cb8107..5de3f7e4 100644
--- a/api/layer/locking_test.go
+++ b/api/layer/locking_test.go
@@ -12,8 +12,11 @@ import (
func TestObjectLockAttributes(t *testing.T) {
tc := prepareContext(t)
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
- BktInfo: tc.bktInfo,
- Settings: &data.BucketSettings{Versioning: data.VersioningEnabled},
+ BktInfo: tc.bktInfo,
+ Settings: &data.BucketSettings{Versioning: data.Versioning{
+ VersioningStatus: data.VersioningEnabled,
+ MFADeleteStatus: data.MFADeleteDisabled,
+ }},
})
require.NoError(t, err)
diff --git a/api/layer/system_object.go b/api/layer/system_object.go
index c19127a9..48ef15ef 100644
--- a/api/layer/system_object.go
+++ b/api/layer/system_object.go
@@ -296,7 +296,10 @@ func (n *Layer) GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo)
if !errors.Is(err, tree.ErrNodeNotFound) {
return nil, err
}
- settings = &data.BucketSettings{Versioning: data.VersioningUnversioned}
+ settings = &data.BucketSettings{Versioning: data.Versioning{
+ VersioningStatus: data.VersioningUnversioned,
+ MFADeleteStatus: data.MFADeleteDisabled,
+ }}
n.reqLogger(ctx).Debug(logs.BucketSettingsNotFoundUseDefaults, logs.TagField(logs.TagDatapath))
}
diff --git a/api/layer/versioning_test.go b/api/layer/versioning_test.go
index 51d35cd6..8b83be93 100644
--- a/api/layer/versioning_test.go
+++ b/api/layer/versioning_test.go
@@ -196,8 +196,11 @@ func prepareContext(t *testing.T, cachesConfig ...*CachesConfig) *testContext {
func TestSimpleVersioning(t *testing.T) {
tc := prepareContext(t)
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
- BktInfo: tc.bktInfo,
- Settings: &data.BucketSettings{Versioning: data.VersioningEnabled},
+ BktInfo: tc.bktInfo,
+ Settings: &data.BucketSettings{Versioning: data.Versioning{
+ VersioningStatus: data.VersioningEnabled,
+ MFADeleteStatus: data.MFADeleteDisabled,
+ }},
})
require.NoError(t, err)
@@ -234,7 +237,10 @@ func TestSimpleNoVersioning(t *testing.T) {
func TestVersioningDeleteObject(t *testing.T) {
tc := prepareContext(t)
- settings := &data.BucketSettings{Versioning: data.VersioningEnabled}
+ settings := &data.BucketSettings{Versioning: data.Versioning{
+ VersioningStatus: data.VersioningEnabled,
+ MFADeleteStatus: data.MFADeleteDisabled,
+ }}
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
BktInfo: tc.bktInfo,
Settings: settings,
@@ -256,7 +262,10 @@ func TestGetUnversioned(t *testing.T) {
objContent := []byte("content obj1 v1")
objInfo := tc.putObject(objContent)
- settings := &data.BucketSettings{Versioning: data.VersioningUnversioned}
+ settings := &data.BucketSettings{Versioning: data.Versioning{
+ VersioningStatus: data.VersioningUnversioned,
+ MFADeleteStatus: data.MFADeleteDisabled,
+ }}
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
BktInfo: tc.bktInfo,
Settings: settings,
@@ -270,7 +279,10 @@ func TestGetUnversioned(t *testing.T) {
func TestVersioningDeleteSpecificObjectVersion(t *testing.T) {
tc := prepareContext(t)
- settings := &data.BucketSettings{Versioning: data.VersioningEnabled}
+ settings := &data.BucketSettings{Versioning: data.Versioning{
+ VersioningStatus: data.VersioningEnabled,
+ MFADeleteStatus: data.MFADeleteDisabled,
+ }}
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
BktInfo: tc.bktInfo,
Settings: settings,
diff --git a/cmd/s3-gw/app.go b/cmd/s3-gw/app.go
index d81babf2..05857761 100644
--- a/cmd/s3-gw/app.go
+++ b/cmd/s3-gw/app.go
@@ -18,6 +18,7 @@ import (
"time"
"git.frostfs.info/TrueCloudLab/frostfs-contract/commonclient"
+ "git.frostfs.info/TrueCloudLab/frostfs-mfa/mfa"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
grpctracing "git.frostfs.info/TrueCloudLab/frostfs-observability/tracing/grpc"
qostagging "git.frostfs.info/TrueCloudLab/frostfs-qos/tagging"
@@ -77,6 +78,8 @@ type (
frostfsid *frostfsid.FrostFSID
+ mfaManager *mfa.Manager
+
policyStorage *policy.Storage
servers []Server
@@ -246,6 +249,7 @@ func (a *App) init(ctx context.Context) {
a.setRuntimeParameters()
a.initFrostfsID(ctx)
a.initPolicyStorage(ctx)
+ a.initMfaManager(ctx)
a.initAPI(ctx)
a.initMetrics()
a.initServers(ctx)
@@ -1197,15 +1201,56 @@ func getFrostfsIDCacheConfig(v *viper.Viper, l *zap.Logger) *cache.Config {
return cacheCfg
}
+func (a *App) initMfaManager(ctx context.Context) *mfa.Manager {
+ var err error
+ var mfaCnrInfo *data.BucketInfo
+
+ if a.config().IsSet(cfgContainersMFA) {
+ mfaCnrInfo, err = a.fetchContainerInfo(ctx, cfgContainersMFA)
+ if err != nil {
+ a.log.Fatal(logs.CouldNotFetchMFAContainerInfo, zap.Error(err))
+ }
+ }
+
+ mfaConfig, err := a.fetchMFAConfig(mfaCnrInfo.CID)
+ if err != nil {
+ a.log.Fatal(logs.CouldNotInitMFAClient, zap.Error(err))
+ }
+
+ manager, err := mfa.NewManager(mfaConfig)
+ if err != nil {
+ a.log.Fatal(logs.CouldNotInitMFAClient, zap.Error(err))
+ }
+ return manager
+}
+
func (a *App) initHandler() {
var err error
- a.api, err = handler.New(a.log, a.obj, a.settings, a.policyStorage, a.frostfsid)
+ a.api, err = handler.New(a.log, a.obj, a.settings, a.policyStorage, a.frostfsid, a.mfaManager)
if err != nil {
a.log.Fatal(logs.CouldNotInitializeAPIHandler, zap.Error(err), logs.TagField(logs.TagApp))
}
}
+func (a *App) fetchMFAConfig(id cid.ID) (mfa.Config, error) {
+ mfaFrostFS := frostfs.NewMFAFrostFS(frostfs.MFAFrostFSConfig{
+ Pool: a.pool,
+ TreePool: a.treePool,
+ Key: a.key,
+ Logger: a.log,
+ })
+
+ config := mfa.Config{
+ Storage: mfaFrostFS,
+ Unlocker: mfaFrostFS,
+ Container: id,
+ Logger: a.log,
+ }
+
+ return config, nil
+}
+
func (a *App) getServer(address string) Server {
for i := range a.servers {
if a.servers[i].Address() == address {
diff --git a/cmd/s3-gw/app_settings.go b/cmd/s3-gw/app_settings.go
index 3c02c554..feceb48b 100644
--- a/cmd/s3-gw/app_settings.go
+++ b/cmd/s3-gw/app_settings.go
@@ -224,6 +224,7 @@ const (
cfgContainersCORS = "containers.cors"
cfgContainersLifecycle = "containers.lifecycle"
cfgContainersAccessBox = "containers.accessbox"
+ cfgContainersMFA = "containers.mfa"
// Multinet.
cfgMultinetEnabled = "multinet.enabled"
diff --git a/config/config.env b/config/config.env
index 53171673..23e139dd 100644
--- a/config/config.env
+++ b/config/config.env
@@ -273,6 +273,7 @@ S3_GW_RETRY_STRATEGY=exponential
# Containers properties
S3_GW_CONTAINERS_CORS=AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
S3_GW_CONTAINERS_LIFECYCLE=AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
+S3_GW_CONTAINERS_MFA=HV9h4zbp7Dti2VXef2oFSsBSRyJUR6NfMeswuv12fjZu
# Multinet properties
# Enable multinet support
diff --git a/config/config.yaml b/config/config.yaml
index 28e83654..d19c8974 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -322,6 +322,7 @@ retry:
containers:
cors: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
lifecycle: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
+ mfa: HV9h4zbp7Dti2VXef2oFSsBSRyJUR6NfMeswuv12fjZu
# Multinet properties
multinet:
diff --git a/docs/configuration.md b/docs/configuration.md
index d3662fba..872724fa 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -859,6 +859,7 @@ containers:
cors: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
lifecycle: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
accessbox: ExnA1gSY3kzgomi2wJxNyWo1ytWv9VAKXRE55fNXEPL2
+ mfa: HV9h4zbp7Dti2VXef2oFSsBSRyJUR6NfMeswuv12fjZu
```
| Parameter | Type | SIGHUP reload | Default value | Description |
@@ -866,6 +867,7 @@ containers:
| `cors` | `string` | no | | Container name for CORS configurations. |
| `lifecycle` | `string` | no | | Container name for lifecycle configurations. If not set, container of the bucket is used. |
| `accessbox` | `string` | no | | Container name to lookup accessbox if custom aws credentials is used. If not set, custom credentials are not supported. |
+| `mfa` | `string` | no | | Container name for virtual MFA devices. If not set, MFADeleteBucket are not supported. |
# `vhs` section
diff --git a/go.mod b/go.mod
index cfcdb834..78f7965e 100644
--- a/go.mod
+++ b/go.mod
@@ -4,9 +4,10 @@ go 1.22
require (
git.frostfs.info/TrueCloudLab/frostfs-contract v0.20.1-0.20241022094040-5f956751d48b
- git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20241125133852-37bd75821121
+ git.frostfs.info/TrueCloudLab/frostfs-mfa v0.0.0-20250314064555-7bca08b33907
+ git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20250212111929-d34e1329c824
git.frostfs.info/TrueCloudLab/frostfs-qos v0.0.0-20250227072915-25102d1e1aa3
- git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20250228093256-2b8329e026c7
+ git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20250310162458-a262a0038f7d
git.frostfs.info/TrueCloudLab/multinet v0.0.0-20241015075604-6cb0d80e0972
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240822104152-a3bc3099bd5b
git.frostfs.info/TrueCloudLab/zapjournald v0.0.0-20240124114243-cb2e66427d02
@@ -22,25 +23,26 @@ require (
github.com/mr-tron/base58 v1.2.0
github.com/nspcc-dev/neo-go v0.106.3
github.com/panjf2000/ants/v2 v2.5.0
- github.com/prometheus/client_golang v1.19.0
- github.com/prometheus/client_model v0.5.0
+ github.com/pquerna/otp v1.4.0
+ github.com/prometheus/client_golang v1.20.2
+ github.com/prometheus/client_model v0.6.1
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.15.0
github.com/ssgreg/journald v1.0.0
- github.com/stretchr/testify v1.9.0
+ github.com/stretchr/testify v1.10.0
github.com/trailofbits/go-fuzz-utils v0.0.0-20230413173806-58c38daa3cb4
- github.com/urfave/cli/v2 v2.27.2
+ github.com/urfave/cli/v2 v2.27.4
go.opentelemetry.io/otel v1.31.0
go.opentelemetry.io/otel/trace v1.31.0
go.uber.org/zap v1.27.0
- golang.org/x/crypto v0.31.0
- golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
+ golang.org/x/crypto v0.33.0
+ golang.org/x/exp v0.0.0-20240823005443-9b4947da3948
golang.org/x/net v0.30.0
- golang.org/x/sys v0.28.0
- golang.org/x/text v0.21.0
+ golang.org/x/sys v0.30.0
+ golang.org/x/text v0.22.0
google.golang.org/grpc v1.69.2
- google.golang.org/protobuf v1.36.1
+ google.golang.org/protobuf v1.36.5
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
@@ -65,23 +67,25 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.6 // indirect
github.com/beorn7/perks v1.0.1 // indirect
+ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
- github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
+ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/snappy v0.0.1 // indirect
- github.com/gorilla/websocket v1.5.1 // indirect
+ github.com/gorilla/websocket v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
- github.com/holiman/uint256 v1.2.4 // indirect
+ github.com/holiman/uint256 v1.3.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/ipfs/go-cid v0.0.7 // indirect
github.com/josharian/intern v1.0.0 // indirect
+ github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
@@ -93,14 +97,15 @@ require (
github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect
- github.com/nspcc-dev/go-ordered-json v0.0.0-20240301084351-0246b013f8b2 // indirect
- github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240727093519-1a48f1ce43ec // indirect
- github.com/nspcc-dev/rfc6979 v0.2.1 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/nspcc-dev/go-ordered-json v0.0.0-20240830112754-291b000d1f3b // indirect
+ github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20241212130705-ea0a6114d2d6 // indirect
+ github.com/nspcc-dev/rfc6979 v0.2.3 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
- github.com/prometheus/common v0.48.0 // indirect
- github.com/prometheus/procfs v0.12.0 // indirect
+ github.com/prometheus/common v0.55.0 // indirect
+ github.com/prometheus/procfs v0.15.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.9.3 // indirect
@@ -109,8 +114,8 @@ require (
github.com/subosito/gotenv v1.4.2 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 // indirect
github.com/twmb/murmur3 v1.1.8 // indirect
- github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
- go.etcd.io/bbolt v1.3.9 // indirect
+ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
+ go.etcd.io/bbolt v1.3.11 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 // indirect
@@ -118,8 +123,8 @@ require (
go.opentelemetry.io/otel/sdk v1.31.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
- golang.org/x/sync v0.10.0 // indirect
- golang.org/x/term v0.27.0 // indirect
+ golang.org/x/sync v0.11.0 // indirect
+ golang.org/x/term v0.29.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
diff --git a/go.sum b/go.sum
index 67a78962..9d539a66 100644
--- a/go.sum
+++ b/go.sum
@@ -40,12 +40,14 @@ git.frostfs.info/TrueCloudLab/frostfs-contract v0.20.1-0.20241022094040-5f956751
git.frostfs.info/TrueCloudLab/frostfs-contract v0.20.1-0.20241022094040-5f956751d48b/go.mod h1:5fSm/l5xSjGWqsPUffSdboiGFUHa7y/1S0fvxzQowN8=
git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 h1:FxqFDhQYYgpe41qsIHVOcdzSVCB8JNSfPG7Uk4r2oSk=
git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0/go.mod h1:RUIKZATQLJ+TaYQa60X2fTDwfuhMfm8Ar60bQ5fr+vU=
-git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20241125133852-37bd75821121 h1:/Z8DfbLZXp7exUQWUKoG/9tbFdI9d5lV1qSReaYoG8I=
-git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20241125133852-37bd75821121/go.mod h1:kbwB4v2o6RyOfCo9kEFeUDZIX3LKhmS0yXPrtvzkQ1g=
+git.frostfs.info/TrueCloudLab/frostfs-mfa v0.0.0-20250314064555-7bca08b33907 h1:lp1NIXoDoekIvP0z+206CdLsnzflCgtZj1RwoWMSprU=
+git.frostfs.info/TrueCloudLab/frostfs-mfa v0.0.0-20250314064555-7bca08b33907/go.mod h1:zP9A73v7XhTJjPIT5fuGxC+c/WBMBY1hhHO+U1DGSxg=
+git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20250212111929-d34e1329c824 h1:Mxw1c/8t96vFIUOffl28lFaHKi413oCBfLMGJmF9cFA=
+git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20250212111929-d34e1329c824/go.mod h1:kbwB4v2o6RyOfCo9kEFeUDZIX3LKhmS0yXPrtvzkQ1g=
git.frostfs.info/TrueCloudLab/frostfs-qos v0.0.0-20250227072915-25102d1e1aa3 h1:QnAt5b2R6+hQthMOIn5ECfLAlVD8IAE5JRm1NCCOmuE=
git.frostfs.info/TrueCloudLab/frostfs-qos v0.0.0-20250227072915-25102d1e1aa3/go.mod h1:PCijYq4oa8vKtIEcUX6jRiszI6XAW+nBwU+T1kB4d1U=
-git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20250228093256-2b8329e026c7 h1:T7r38zZ/aT1xTp+AxhizfukW10Rq3WQ5/m3moLGVnSk=
-git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20250228093256-2b8329e026c7/go.mod h1:aQpPWfG8oyfJ2X+FenPTJpSRWZjwcP5/RAtkW+/VEX8=
+git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20250310162458-a262a0038f7d h1:YrBjEkuc+q9RdqWmpxRL3DjYrO+9/YOYYIbeq0EVnQs=
+git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20250310162458-a262a0038f7d/go.mod h1:aQpPWfG8oyfJ2X+FenPTJpSRWZjwcP5/RAtkW+/VEX8=
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/multinet v0.0.0-20241015075604-6cb0d80e0972 h1:/960fWeyn2AFHwQUwDsWB3sbP6lTEnFnMzLMM6tx6N8=
@@ -106,6 +108,8 @@ github.com/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5M
github.com/bits-and-blooms/bitset v1.8.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
+github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
+github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -127,8 +131,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -213,8 +217,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
-github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
-github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -223,8 +227,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
-github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
+github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs=
+github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -237,6 +241,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
@@ -247,6 +253,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
@@ -282,16 +290,18 @@ github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsC
github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nspcc-dev/dbft v0.2.0 h1:sDwsQES600OSIMncV176t2SX5OvB14lzeOAyKFOkbMI=
github.com/nspcc-dev/dbft v0.2.0/go.mod h1:oFE6paSC/yfFh9mcNU6MheMGOYXK9+sPiRk3YMoz49o=
-github.com/nspcc-dev/go-ordered-json v0.0.0-20240301084351-0246b013f8b2 h1:mD9hU3v+zJcnHAVmHnZKt3I++tvn30gBj2rP2PocZMk=
-github.com/nspcc-dev/go-ordered-json v0.0.0-20240301084351-0246b013f8b2/go.mod h1:U5VfmPNM88P4RORFb6KSUVBdJBDhlqggJZYGXGPxOcc=
+github.com/nspcc-dev/go-ordered-json v0.0.0-20240830112754-291b000d1f3b h1:DRG4cRqIOmI/nUPggMgR92Jxt63Lxsuz40m5QpdvYXI=
+github.com/nspcc-dev/go-ordered-json v0.0.0-20240830112754-291b000d1f3b/go.mod h1:d3cUseu4Asxfo9/QA/w4TtGjM0AbC9ynyab+PfH+Bso=
github.com/nspcc-dev/neo-go v0.106.3 h1:HEyhgkjQY+HfBzotMJ12xx2VuOUphkngZ4kEkjvXDtE=
github.com/nspcc-dev/neo-go v0.106.3/go.mod h1:3vEwJ2ld12N7HRGCaH/l/7EwopplC/+8XdIdPDNmD/M=
-github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240727093519-1a48f1ce43ec h1:vDrbVXF2+2uP0RlkZmem3QYATcXCu9BzzGGCNsNcK7Q=
-github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240727093519-1a48f1ce43ec/go.mod h1:/vrbWSHc7YS1KSYhVOyyeucXW/e+1DkVBOgnBEXUCeY=
-github.com/nspcc-dev/rfc6979 v0.2.1 h1:8wWxkamHWFmO790GsewSoKUSJjVnL1fmdRpokU/RgRM=
-github.com/nspcc-dev/rfc6979 v0.2.1/go.mod h1:Tk7h5kyUWkhjyO3zUgFFhy1v2vQv3BvQEntakdtqrWc=
+github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20241212130705-ea0a6114d2d6 h1:rTnsU+Y/bP1bLN/SNWmOKEexmSeniMQe5bOJxXNbXgg=
+github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20241212130705-ea0a6114d2d6/go.mod h1:kVLzmbeJJdbIPF2bUYhD8YppIiLXnRQj5yqNZvzbOL0=
+github.com/nspcc-dev/rfc6979 v0.2.3 h1:QNVykGZ3XjFwM/88rGfV3oj4rKNBy+nYI6jM7q19hDI=
+github.com/nspcc-dev/rfc6979 v0.2.3/go.mod h1:q3sCL1Ed7homjqYK8KmFSzEmm+7Ngyo7PePbZanhaDE=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -311,15 +321,17 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
-github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
+github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
+github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
+github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg=
+github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
-github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
-github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
-github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
-github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
-github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
+github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
@@ -347,14 +359,15 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 h1:xQdMZ1WLrgkkvOZ/LDQxjVxMLdby7osSh4ZEVa5sIjs=
@@ -363,16 +376,16 @@ github.com/trailofbits/go-fuzz-utils v0.0.0-20230413173806-58c38daa3cb4 h1:GpfJ7
github.com/trailofbits/go-fuzz-utils v0.0.0-20230413173806-58c38daa3cb4/go.mod h1:f3jBhpWvuZmue0HZK52GzRHJOYHYSILs/c8+K2S/J+o=
github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
-github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
-github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
-github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
-github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
+github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
+github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
+github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
+github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
-go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
+go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
+go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -412,8 +425,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
-golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
+golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -424,8 +437,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
-golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
+golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
+golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -449,8 +462,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
-golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
+golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -506,8 +519,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
-golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
+golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -551,19 +564,19 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
-golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
+golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -614,8 +627,8 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
+golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -716,8 +729,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
-google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
+google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
diff --git a/internal/frostfs/mfa.go b/internal/frostfs/mfa.go
new file mode 100644
index 00000000..f3caba4d
--- /dev/null
+++ b/internal/frostfs/mfa.go
@@ -0,0 +1,440 @@
+package frostfs
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+
+ "git.frostfs.info/TrueCloudLab/frostfs-mfa/mfa"
+ "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs"
+ "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
+ "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
+ apitree "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/tree"
+ cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
+ oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
+ "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
+ treepool "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool/tree"
+ "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
+ "go.uber.org/zap"
+)
+
+// MFAFrostFS is a mediator which implements mfa.Storage through pool.Pool and treepool.Pool.
+type MFAFrostFS struct {
+ frostFS *FrostFS
+ treePool *treepool.Pool
+ log *zap.Logger
+ key *keys.PrivateKey
+}
+
+func (m *MFAFrostFS) PrivateKey() *keys.PrivateKey {
+ return m.key
+}
+
+func (m *MFAFrostFS) PublicKeys() []*keys.PublicKey {
+ // TODO implement me
+ panic("implement me")
+}
+
+func (m *MFAFrostFS) CreateObject(ctx context.Context, create mfa.PrmObjectCreate) (oid.ID, error) {
+ object, err := m.frostFS.CreateObject(ctx, frostfs.PrmObjectCreate{
+ Container: create.Container,
+ Payload: bytes.NewReader(create.Payload),
+ Filepath: create.FilePath,
+ PayloadSize: uint64(len(create.Payload)),
+ WithoutHomomorphicHash: true,
+ })
+ if err != nil {
+ return [32]byte{}, err
+ }
+ return object.ObjectID, nil
+}
+
+func (m *MFAFrostFS) DeleteObject(ctx context.Context, address oid.Address) error {
+ prm := frostfs.PrmObjectDelete{
+ Container: address.Container(),
+ Object: address.Object(),
+ }
+
+ return m.frostFS.DeleteObject(ctx, prm)
+}
+
+func (m *MFAFrostFS) SetTreeNode(ctx context.Context, cnrID cid.ID, name string, meta map[string]string) (*mfa.TreeMultiNode, error) {
+ if len(name) == 0 {
+ return nil, errors.New("tree node name must not be empty")
+ }
+
+ path := pathFromName(name)
+ meta[fileNameKey] = path[len(path)-1]
+
+ multiNode, err := m.getTreeNode(ctx, cnrID, path)
+ isErrNotFound := errors.Is(err, mfa.ErrTreeNodeNotFound)
+ if err != nil && !isErrNotFound {
+ return nil, fmt.Errorf("couldn't get node to check: %w", err)
+ }
+
+ if isErrNotFound {
+ prmAdd := treepool.AddNodeByPathParams{
+ CID: cnrID,
+ TreeID: mfaTreeName,
+ Path: path[:len(path)-1],
+ Meta: meta,
+ PathAttribute: fileNameKey,
+ }
+
+ if _, err = m.treePool.AddNodeByPath(ctx, prmAdd); err != nil {
+ return nil, fmt.Errorf("add node by path: %w", err)
+ }
+
+ return &mfa.TreeMultiNode{Current: mfa.TreeNode{Meta: meta}}, nil
+ }
+
+ node := multiNode.Latest()
+ prmMove := treepool.MoveNodeParams{
+ CID: cnrID,
+ TreeID: mfaTreeName,
+ NodeID: node.ID,
+ ParentID: node.ParentID,
+ Meta: meta,
+ }
+
+ if err = m.treePool.MoveNode(ctx, prmMove); err != nil {
+ return nil, fmt.Errorf("move node: %w", err)
+ }
+
+ mfaMultiNode := &mfa.TreeMultiNode{Current: mfa.TreeNode{Meta: meta}}
+ for _, old := range m.cleanOldNodes(ctx, multiNode.Old(), cnrID) {
+ mfaMultiNode.Old = append(mfaMultiNode.Old, &mfa.TreeNode{Meta: old.Meta})
+ }
+
+ return mfaMultiNode, nil
+}
+
+func (m *MFAFrostFS) GetTreeNode(ctx context.Context, cnrID cid.ID, name string) (*mfa.TreeMultiNode, error) {
+ multiNode, err := m.getTreeNode(ctx, cnrID, pathFromName(name))
+ if err != nil {
+ return nil, fmt.Errorf("couldn't get node: %w", err)
+ }
+
+ return multiNode.ToMFAMultiNode(), nil
+}
+
+func (m *MFAFrostFS) DeleteTreeNode(ctx context.Context, cnrID cid.ID, name string) ([]*mfa.TreeNode, error) {
+ multiNode, err := m.getTreeNode(ctx, cnrID, pathFromName(name))
+ if err != nil {
+ return nil, fmt.Errorf("couldn't get node: %w", err)
+ }
+
+ res := make([]*mfa.TreeNode, 0, len(multiNode.nodes))
+ for _, node := range m.cleanOldNodes(ctx, multiNode.nodes, cnrID) {
+ res = append(res, &mfa.TreeNode{Meta: node.Meta})
+ }
+
+ if len(res) != len(multiNode.nodes) {
+ return res, fmt.Errorf("couldn't remove all mfa multi nodes '%s'", name)
+ }
+
+ return res, nil
+}
+
+func (m *MFAFrostFS) GetTreeNodes(ctx context.Context, cnrID cid.ID, prefix string) ([]*mfa.TreeNode, error) {
+ rootID := []uint64{0}
+ if len(prefix) != 0 {
+ var err error
+ rootID, err = m.getPrefixNodeID(ctx, cnrID, pathFromName(prefix))
+ if err != nil {
+ if errors.Is(err, mfa.ErrTreeNodeNotFound) {
+ return nil, nil
+ }
+ return nil, err
+ }
+ }
+
+ prm := treepool.GetSubTreeParams{
+ CID: cnrID,
+ TreeID: mfaTreeName,
+ RootID: rootID,
+ }
+
+ subTreeCli, err := m.treePool.GetSubTree(ctx, prm) // todo use streaming https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/issues/561
+ if err != nil {
+ return nil, err
+ }
+
+ allNodes, err := subTreeCli.ReadAll()
+ if err != nil {
+ return nil, err
+ }
+
+ unique := filterUnique(allNodes)
+ res := make([]*mfa.TreeNode, 0, len(unique))
+ for _, v := range unique {
+ if isIntermediate(v.GetMeta()) {
+ continue
+ }
+ meta := make(map[string]string, len(v.GetMeta()))
+ for _, kv := range v.GetMeta() {
+ meta[kv.GetKey()] = string(kv.GetValue())
+ }
+ res = append(res, &mfa.TreeNode{Meta: meta})
+ }
+
+ return res, nil
+}
+
+func filterUnique(allNodes []*apitree.GetSubTreeResponseBody) map[string]*apitree.GetSubTreeResponseBody {
+ res := make(map[string]*apitree.GetSubTreeResponseBody, len(allNodes))
+ for _, node := range allNodes {
+ var name string
+ for _, kv := range node.GetMeta() {
+ if kv.GetKey() == fileNameKey {
+ name = string(kv.GetValue())
+ }
+ }
+
+ data, ok := res[name]
+ if !ok || getMaxTimestamp(data.GetTimestamp()) < getMaxTimestamp(node.GetTimestamp()) {
+ res[name] = node
+ }
+ }
+
+ return res
+}
+
+func getMaxTimestamp(timestamps []uint64) uint64 {
+ var maxTimestamp uint64
+
+ for _, timestamp := range timestamps {
+ if timestamp > maxTimestamp {
+ maxTimestamp = timestamp
+ }
+ }
+
+ return maxTimestamp
+}
+
+type MFAFrostFSConfig struct {
+ Pool *pool.Pool
+ TreePool *treepool.Pool
+ Key *keys.PrivateKey
+ Logger *zap.Logger
+}
+
+type treeNode struct {
+ ID uint64
+ ParentID uint64
+ TimeStamp uint64
+ Meta map[string]string
+}
+
+type multiSystemNode struct {
+ // the first element is latest
+ nodes []*treeNode
+}
+
+const (
+ fileNameKey = "FileName"
+ mfaTreeName = "mfa"
+)
+
+// NewMFAFrostFS creates new MFAFrostFS using provided pool.Pool.
+func NewMFAFrostFS(cfg MFAFrostFSConfig) *MFAFrostFS {
+ return &MFAFrostFS{
+ frostFS: NewFrostFS(cfg.Pool, cfg.Key),
+ treePool: cfg.TreePool,
+ log: cfg.Logger,
+ key: cfg.Key,
+ }
+}
+
+func (m *MFAFrostFS) GetObject(ctx context.Context, addr oid.Address) ([]byte, error) {
+ res, err := m.frostFS.GetObject(ctx, frostfs.PrmObjectGet{
+ Container: addr.Container(),
+ Object: addr.Object(),
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ defer func() {
+ if closeErr := res.Payload.Close(); closeErr != nil {
+ middleware.GetReqLog(ctx).Warn(logs.CloseMFAObjectPayload, zap.Error(closeErr), logs.TagField(logs.TagExternalStorage))
+ }
+ }()
+
+ return io.ReadAll(res.Payload)
+}
+
+func (m *MFAFrostFS) getTreeNode(ctx context.Context, cnrID cid.ID, path []string) (*multiSystemNode, error) {
+ prmGetNodes := treepool.GetNodesParams{
+ CID: cnrID,
+ TreeID: mfaTreeName,
+ Path: path,
+ PathAttribute: fileNameKey,
+ LatestOnly: true,
+ AllAttrs: true,
+ }
+
+ nodes, err := m.treePool.GetNodes(ctx, prmGetNodes)
+ if err != nil {
+ if errors.Is(err, treepool.ErrNodeNotFound) {
+ return nil, fmt.Errorf("%s: %s", "mfa.ErrTreeNodeNotFound", err.Error())
+ // return nil, fmt.Errorf("%w: %s", mfa.ErrTreeNodeNotFound, err.Error())
+ }
+ return nil, fmt.Errorf("get nodes: %w", err)
+ }
+
+ if len(nodes) == 0 {
+ return nil, mfa.ErrTreeNodeNotFound
+ }
+ if len(nodes) != 1 {
+ m.log.Warn(logs.FoundMultiNode, zap.Strings("path", path), logs.TagField(logs.TagExternalStorageTree))
+ }
+
+ return newMultiNode(nodes)
+}
+
+func (m *MFAFrostFS) cleanOldNodes(ctx context.Context, nodes []*treeNode, cnrID cid.ID) []*treeNode {
+ res := make([]*treeNode, 0, len(nodes))
+
+ for _, node := range nodes {
+ if err := m.removeTreeNode(ctx, cnrID, node.ID); err != nil {
+ m.log.Warn(logs.FailedToRemoveOldTreeMultiNodes, zap.String("FileName", node.Meta[fileNameKey]), zap.Uint64("id", node.ID), logs.TagField(logs.TagExternalStorageTree))
+ } else {
+ res = append(res, node)
+ }
+ }
+
+ return res
+}
+
+func (m *MFAFrostFS) removeTreeNode(ctx context.Context, cnrID cid.ID, nodeID uint64) error {
+ prmRemoveNode := treepool.RemoveNodeParams{
+ CID: cnrID,
+ TreeID: mfaTreeName,
+ NodeID: nodeID,
+ }
+
+ err := m.treePool.RemoveNode(ctx, prmRemoveNode)
+ if err != nil {
+ if errors.Is(err, treepool.ErrNodeNotFound) {
+ return fmt.Errorf("%w: %s", mfa.ErrTreeNodeNotFound, err.Error())
+ }
+ return fmt.Errorf("remove node: %w", err)
+ }
+
+ return nil
+}
+
+func (m *MFAFrostFS) getPrefixNodeID(ctx context.Context, cnrID cid.ID, prefixPath []string) ([]uint64, error) {
+ p := treepool.GetNodesParams{
+ CID: cnrID,
+ TreeID: mfaTreeName,
+ Path: prefixPath,
+ LatestOnly: false,
+ AllAttrs: true,
+ }
+
+ nodes, err := m.treePool.GetNodes(ctx, p)
+ if err != nil {
+ if errors.Is(err, treepool.ErrNodeNotFound) {
+ return nil, fmt.Errorf("%w: %s", mfa.ErrTreeNodeNotFound, err.Error())
+ }
+ return nil, err
+ }
+
+ var intermediateNodes []uint64
+ for _, node := range nodes {
+ if isIntermediate(node.GetMeta()) {
+ intermediateNodes = append(intermediateNodes, node.GetNodeID())
+ }
+ }
+
+ if len(intermediateNodes) == 0 {
+ return nil, treepool.ErrNodeNotFound
+ }
+
+ return intermediateNodes, nil
+}
+
+func isIntermediate(meta []*apitree.KeyValue) bool {
+ if len(meta) != 1 {
+ return false
+ }
+
+ return meta[0].GetKey() == fileNameKey
+}
+
+func newMultiNode(nodes []*apitree.GetNodeByPathResponseInfo) (*multiSystemNode, error) {
+ var (
+ err error
+ index int
+ maxTimestamp uint64
+ )
+
+ if len(nodes) == 0 {
+ return nil, errors.New("multi node must have at least one node")
+ }
+
+ treeNodes := make([]*treeNode, len(nodes))
+
+ for i, node := range nodes {
+ if treeNodes[i] = newTreeNode(node); err != nil {
+ return nil, fmt.Errorf("parse tree node response: %w", err)
+ }
+
+ if maxTimestamp < node.GetTimestamp() {
+ index = i
+ maxTimestamp = node.GetTimestamp()
+ }
+ }
+
+ treeNodes[0], treeNodes[index] = treeNodes[index], treeNodes[0]
+
+ return &multiSystemNode{
+ nodes: treeNodes,
+ }, nil
+}
+
+func (m *multiSystemNode) ToMFAMultiNode() *mfa.TreeMultiNode {
+ res := &mfa.TreeMultiNode{
+ Current: mfa.TreeNode{Meta: m.nodes[0].Meta},
+ Old: make([]*mfa.TreeNode, len(m.nodes[1:])),
+ }
+
+ for i, node := range m.nodes[1:] {
+ res.Old[i] = &mfa.TreeNode{Meta: node.Meta}
+ }
+
+ return res
+}
+
+func newTreeNode(nodeInfo *apitree.GetNodeByPathResponseInfo) *treeNode {
+ tNode := &treeNode{
+ ID: nodeInfo.GetNodeID(),
+ ParentID: nodeInfo.GetParentID(),
+ TimeStamp: nodeInfo.GetTimestamp(),
+ Meta: make(map[string]string, len(nodeInfo.GetMeta())),
+ }
+
+ for _, kv := range nodeInfo.GetMeta() {
+ tNode.Meta[kv.GetKey()] = string(kv.GetValue())
+ }
+
+ return tNode
+}
+
+func (m *multiSystemNode) Old() []*treeNode {
+ return m.nodes[1:]
+}
+
+func (m *multiSystemNode) Latest() *treeNode {
+ return m.nodes[0]
+}
+
+// pathFromName splits name by '/'.
+func pathFromName(name string) []string {
+ return strings.Split(name, "/")
+}
diff --git a/internal/logs/logs.go b/internal/logs/logs.go
index c0db6fe3..df3cb42e 100644
--- a/internal/logs/logs.go
+++ b/internal/logs/logs.go
@@ -125,6 +125,8 @@ const (
FailedToParseAddressInTreeNode = "failed to parse object addr in tree node"
UnexpectedMultiNodeIDsInSubTreeMultiParts = "unexpected multi node ids in sub tree multi parts"
FoundSeveralSystemNodes = "found several system nodes"
+ CouldNotFetchMFAContainerInfo = "couldn't fetch mfa container info"
+ CouldNotInitMFAClient = "couldn't init MFA client"
BucketLifecycleNodeHasMultipleIDs = "bucket lifecycle node has multiple ids"
UploadPart = "upload part"
FailedToSubmitTaskToPool = "failed to submit task to pool"
@@ -181,6 +183,7 @@ const (
FailedToDeleteObject = "failed to delete object"
CouldntDeleteLifecycleObject = "couldn't delete lifecycle configuration object"
CouldntGetCORSObjectVersions = "couldn't get cors object versions"
+ CloseMFAObjectPayload = "close MFA object payload"
)
// External blockchain.
@@ -202,6 +205,8 @@ const (
BucketSettingsNodeHasMultipleIDs = "bucket settings node has multiple ids"
GetAllBucketCorsFromTree = "get all bucket cors from tree"
CouldntDeleteBucketCORS = "couldn't delete bucket cors"
+ FoundMultiNode = "found multi node"
+ FailedToRemoveOldTreeMultiNodes = "failed to remove old tree multi nodes"
)
// Authmate.
@@ -229,3 +234,6 @@ const (
FailedToReadHTTPBody = "failed to read http body"
FailedToProcessHTTPBody = "failed to process http body"
)
+
+// IAM Logs.
+const ()
diff --git a/pkg/service/tree/tree.go b/pkg/service/tree/tree.go
index 2b09d9e1..cdbfd0cb 100644
--- a/pkg/service/tree/tree.go
+++ b/pkg/service/tree/tree.go
@@ -90,6 +90,8 @@ var (
const (
versioningKV = "Versioning"
+ mfaDeleteEnabledKV = "MFADelete"
+ mfaSerialNumberKV = "SerialNumber"
cannedACLKV = "cannedACL"
ownerKeyKV = "ownerKey"
lockConfigurationKV = "LockConfiguration"
@@ -504,9 +506,20 @@ func (c *Tree) GetSettingsNode(ctx context.Context, bktInfo *data.BucketInfo) (*
node := multiNode.Latest()
- settings := &data.BucketSettings{Versioning: data.VersioningUnversioned}
+ settings := &data.BucketSettings{Versioning: data.Versioning{
+ VersioningStatus: data.VersioningUnversioned,
+ MFADeleteStatus: data.MFADeleteDisabled,
+ }}
if versioningValue, ok := node.Get(versioningKV); ok {
- settings.Versioning = versioningValue
+ settings.Versioning.VersioningStatus = versioningValue
+ }
+ if mfaDeleteValue, ok := node.Get(mfaDeleteEnabledKV); ok {
+ settings.Versioning.MFADeleteStatus = mfaDeleteValue
+ if settings.MFADeleteEnabled() {
+ if mfaSerialNumberValue, ok := node.Get(mfaSerialNumberKV); ok {
+ settings.Versioning.MFASerialNumber = mfaSerialNumberValue
+ }
+ }
}
if lockConfigurationValue, ok := node.Get(lockConfigurationKV); ok {
@@ -1861,7 +1874,8 @@ func metaFromSettings(settings *data.BucketSettings) map[string]string {
results := make(map[string]string, 3)
results[FileNameKey] = settingsFileName
- results[versioningKV] = settings.Versioning
+ results[versioningKV] = settings.Versioning.VersioningStatus
+ results[mfaDeleteEnabledKV] = settings.Versioning.MFADeleteStatus
results[lockConfigurationKV] = encodeLockConfiguration(settings.LockConfiguration)
results[cannedACLKV] = settings.CannedACL
if settings.OwnerKey != nil {
diff --git a/pkg/service/tree/tree_test.go b/pkg/service/tree/tree_test.go
index 5b7b6656..e4af5d36 100644
--- a/pkg/service/tree/tree_test.go
+++ b/pkg/service/tree/tree_test.go
@@ -119,7 +119,10 @@ func TestTreeServiceSettings(t *testing.T) {
require.NoError(t, err)
settings := &data.BucketSettings{
- Versioning: "Versioning",
+ Versioning: data.Versioning{
+ VersioningStatus: data.VersioningEnabled,
+ MFADeleteStatus: data.MFADeleteDisabled,
+ },
LockConfiguration: &data.ObjectLockConfiguration{
ObjectLockEnabled: "Enabled",
Rule: &data.ObjectLockRule{