[#604] Add MFADelete tests with reworked mfa.Storage implementation

Signed-off-by: Alex Vanin <a.vanin@yadro.com>
Signed-off-by: Pavel Pogodaev <p.pogodaev@yadro.com>
This commit is contained in:
Alexey Vanin 2025-03-19 15:50:49 +03:00 committed by Pavel Pogodaev
parent 0fc56cbfce
commit 7d6e20fdad
17 changed files with 440 additions and 233 deletions

View file

@ -175,7 +175,6 @@ func TestSettingsCacheType(t *testing.T) {
key := "key" key := "key"
settings := &data.BucketSettings{Versioning: data.Versioning{ settings := &data.BucketSettings{Versioning: data.Versioning{
VersioningStatus: data.VersioningEnabled, VersioningStatus: data.VersioningEnabled,
MFADeleteStatus: data.MFADeleteEnabled,
}} }}
err := cache.PutSettings(key, settings) err := cache.PutSettings(key, settings)

View file

@ -163,6 +163,10 @@ func (b BucketSettings) MFADeleteEnabled() bool {
return b.Versioning.MFADeleteStatus == MFADeleteEnabled return b.Versioning.MFADeleteStatus == MFADeleteEnabled
} }
func (b BucketSettings) MFADeleteDisabled() bool {
return b.Versioning.MFADeleteStatus == MFADeleteDisabled
}
func Quote(val string) string { func Quote(val string) string {
return "\"" + val + "\"" return "\"" + val + "\""
} }

View file

@ -143,6 +143,10 @@ const (
// Add new error codes here. // Add new error codes here.
ErrNotSupported ErrNotSupported
ErrMFAAuthNeeded ErrMFAAuthNeeded
ErrCannotPutLifecycleConfiguration
ErrInvalidMFAHeader
ErrMFAAuthIsNotSupported
ErrVersioningNotSpecified
// SSE-S3 related API errors. // SSE-S3 related API errors.
ErrInvalidEncryptionMethod ErrInvalidEncryptionMethod
@ -1802,6 +1806,30 @@ var errorCodes = errorCodeMap{
Description: "AllowedHeader can not have more than one wildcard.", Description: "AllowedHeader can not have more than one wildcard.",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrCannotPutLifecycleConfiguration: {
ErrCode: ErrCannotPutLifecycleConfiguration,
Code: "ErrCannotPutLifecycleConfiguration",
Description: "Cannot put lifecycle configuration on a bucket that has MFA enabled",
HTTPStatusCode: http.StatusBadRequest,
},
ErrMFAAuthIsNotSupported: {
ErrCode: ErrMFAAuthIsNotSupported,
Code: "ErrMFAAuthIsNotSupported",
Description: "MFA Authentication is not supported on a bucket with lifecycle configuration",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidMFAHeader: {
ErrCode: ErrInvalidMFAHeader,
Code: "ErrInvalidMFAHeader",
Description: "Invalid x-amz-mfa header",
HTTPStatusCode: http.StatusBadRequest,
},
ErrVersioningNotSpecified: {
ErrCode: ErrVersioningNotSpecified,
Code: "IllegalVersioningConfigurationException",
Description: "The Versioning element must be specified",
HTTPStatusCode: http.StatusBadRequest,
},
// Add your error structure here. // Add your error structure here.
} }

View file

@ -64,11 +64,11 @@ func TestCopyToItself(t *testing.T) {
copyObject(tc, bktName, objName, objName, CopyMeta{}, http.StatusBadRequest) copyObject(tc, bktName, objName, objName, CopyMeta{}, http.StatusBadRequest)
copyObject(tc, bktName, objName, objName, copyMeta, http.StatusOK) copyObject(tc, bktName, objName, objName, copyMeta, http.StatusOK)
putBucketVersioning(t, tc, bktName, true, "") putBucketVersioning(t, tc, bktName, "Enabled")
copyObject(tc, bktName, objName, objName, CopyMeta{}, http.StatusOK) copyObject(tc, bktName, objName, objName, CopyMeta{}, http.StatusOK)
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, "Suspended")
copyObject(tc, bktName, objName, objName, CopyMeta{}, http.StatusOK) copyObject(tc, bktName, objName, objName, CopyMeta{}, http.StatusOK)
copyObject(tc, bktName, objName, objName, copyMeta, http.StatusOK) copyObject(tc, bktName, objName, objName, copyMeta, http.StatusOK)
} }

View file

@ -100,7 +100,7 @@ func (h *handler) DeleteObjectHandler(w http.ResponseWriter, r *http.Request) {
validate := totp.Validate(token, device.Key.Secret()) validate := totp.Validate(token, device.Key.Secret())
if !validate { if !validate {
h.logAndSendError(ctx, w, "could not validate token", reqInfo, fmt.Errorf("mfa Authentication must be used for this request")) h.logAndSendError(ctx, w, "could not validate token", reqInfo, errors.GetAPIError(errors.ErrMFAAuthNeeded))
return return
} }
} }
@ -222,7 +222,7 @@ func (h *handler) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Re
validate := totp.Validate(token, device.Key.Secret()) validate := totp.Validate(token, device.Key.Secret())
if !validate { if !validate {
h.logAndSendError(ctx, w, "could not validate token", reqInfo, fmt.Errorf("mfa Authentication must be used for this request")) h.logAndSendError(ctx, w, "could not validate token", reqInfo, errors.GetAPIError(errors.ErrMFAAuthNeeded))
return return
} }
} }

View file

@ -10,12 +10,15 @@ import (
"testing" "testing"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-mfa/mfa"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/encryption" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/encryption"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -106,6 +109,27 @@ func TestForceDeleteBucket(t *testing.T) {
deleteBucketForce(t, hc, bktName, http.StatusNoContent, "true") deleteBucketForce(t, hc, bktName, http.StatusNoContent, "true")
} }
func TestForceDeleteBucketWithMFADelete(t *testing.T) {
hc := prepareHandlerContext(t)
bktName, objName, deviceName := "bucket-for-removal", "object-to-delete", "device"
bktInfo := createTestBucket(hc, bktName)
key := createMFADevice(hc, bktName, deviceName)
putBucketVersioningMFADelete(hc, bktName, "Suspended", "Enabled", generateMFAHeader(key, deviceName))
putObject(hc, bktName, objName)
nodeVersion, err := hc.tree.GetUnversioned(hc.context, bktInfo, objName)
require.NoError(t, err)
var addr oid.Address
addr.SetContainer(bktInfo.CID)
addr.SetObject(nodeVersion.OID)
hc.owner = bktInfo.Owner
// force delete bucket fails when MFA Delete enabled
deleteBucketForce(t, hc, bktName, http.StatusConflict, "true")
}
func TestDeleteMultipleObjectCheckUniqueness(t *testing.T) { func TestDeleteMultipleObjectCheckUniqueness(t *testing.T) {
hc := prepareHandlerContext(t) hc := prepareHandlerContext(t)
@ -124,7 +148,7 @@ func TestDeleteObjectsError(t *testing.T) {
bktName, objName := "bucket-for-removal", "object-to-delete" bktName, objName := "bucket-for-removal", "object-to-delete"
bktInfo := createTestBucket(hc, bktName) bktInfo := createTestBucket(hc, bktName)
putBucketVersioning(t, hc, bktName, true, "") putBucketVersioning(t, hc, bktName, "Enabled")
putObject(hc, bktName, objName) putObject(hc, bktName, objName)
@ -302,7 +326,7 @@ func TestDeleteMarkerSuspended(t *testing.T) {
bktName, objName := "bucket-for-removal", "object-to-delete" bktName, objName := "bucket-for-removal", "object-to-delete"
bktInfo, _ := createVersionedBucketAndObject(t, tc, bktName, objName) bktInfo, _ := createVersionedBucketAndObject(t, tc, bktName, objName)
putBucketVersioning(t, tc, bktName, false, "") putBucketVersioning(t, tc, bktName, "Suspended")
t.Run("not create new delete marker if last version is delete marker", func(t *testing.T) { 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) deleteMarkerVersion, isDeleteMarker := deleteObject(t, tc, bktName, objName, emptyVersion)
@ -350,7 +374,7 @@ func TestDeleteObjectCombined(t *testing.T) {
bktName, objName := "bucket-for-removal", "object-to-delete" bktName, objName := "bucket-for-removal", "object-to-delete"
bktInfo, objInfo := createBucketAndObject(tc, bktName, objName) bktInfo, objInfo := createBucketAndObject(tc, bktName, objName)
putBucketVersioning(t, tc, bktName, true, "") putBucketVersioning(t, tc, bktName, "Enabled")
checkFound(t, tc, bktName, objName, emptyVersion) checkFound(t, tc, bktName, objName, emptyVersion)
deleteObject(t, tc, bktName, objName, emptyVersion) deleteObject(t, tc, bktName, objName, emptyVersion)
@ -367,13 +391,13 @@ func TestDeleteObjectSuspended(t *testing.T) {
bktName, objName := "bucket-for-removal", "object-to-delete" bktName, objName := "bucket-for-removal", "object-to-delete"
bktInfo, objInfo := createBucketAndObject(tc, bktName, objName) bktInfo, objInfo := createBucketAndObject(tc, bktName, objName)
putBucketVersioning(t, tc, bktName, true, "") putBucketVersioning(t, tc, bktName, "Enabled")
checkFound(t, tc, bktName, objName, emptyVersion) checkFound(t, tc, bktName, objName, emptyVersion)
deleteObject(t, tc, bktName, objName, emptyVersion) deleteObject(t, tc, bktName, objName, emptyVersion)
checkNotFound(t, tc, bktName, objName, emptyVersion) checkNotFound(t, tc, bktName, objName, emptyVersion)
putBucketVersioning(t, tc, bktName, false, "") putBucketVersioning(t, tc, bktName, "Suspended")
deleteObject(t, tc, bktName, objName, emptyVersion) deleteObject(t, tc, bktName, objName, emptyVersion)
checkNotFound(t, tc, bktName, objName, objInfo.VersionID()) checkNotFound(t, tc, bktName, objName, objInfo.VersionID())
@ -386,7 +410,7 @@ func TestDeleteMarkers(t *testing.T) {
bktName, objName := "bucket-for-removal", "object-to-delete" bktName, objName := "bucket-for-removal", "object-to-delete"
createTestBucket(tc, bktName) createTestBucket(tc, bktName)
putBucketVersioning(t, tc, bktName, true, "") putBucketVersioning(t, tc, bktName, "Enabled")
checkNotFound(t, tc, bktName, objName, emptyVersion) checkNotFound(t, tc, bktName, objName, emptyVersion)
deleteObject(t, tc, bktName, objName, emptyVersion) deleteObject(t, tc, bktName, objName, emptyVersion)
@ -405,7 +429,7 @@ func TestGetHeadDeleteMarker(t *testing.T) {
bktName, objName := "bucket-for-removal", "object-to-delete" bktName, objName := "bucket-for-removal", "object-to-delete"
createTestBucket(hc, bktName) createTestBucket(hc, bktName)
putBucketVersioning(t, hc, bktName, true, "") putBucketVersioning(t, hc, bktName, "Enabled")
putObject(hc, bktName, objName) putObject(hc, bktName, objName)
@ -473,13 +497,47 @@ func TestDeleteBucketByNotOwner(t *testing.T) {
} }
func TestDeleteObjectMFAEnabled(t *testing.T) { func TestDeleteObjectMFAEnabled(t *testing.T) {
hc := prepareHandlerContext(t)
bktName := "bucket-name" bktName := "bucket-name"
deviceName := "mfa" objName := "object-name"
serialNumber := "arn:aws:iam:::mfa/" + deviceName deviceName := "device"
token := "123456"
_ = createVersionedBucketMFAEnabled(hc, bktName, serialNumber+" "+token) t.Run("versioned bucket", func(t *testing.T) {
hc := prepareHandlerContext(t)
bktInfo := createTestBucket(hc, bktName)
key := createMFADevice(hc, bktName, deviceName)
putBucketVersioningMFADelete(hc, bktName, "Enabled", "Enabled", generateMFAHeader(key, deviceName))
objInfo := createTestObject(hc, bktInfo, objName, encryption.Params{})
ver := objInfo.VersionID()
// delete object without MFA with error
deleteObjectErr(t, hc, bktName, objName, ver, apierr.GetAPIError(apierr.ErrMFAAuthNeeded))
// delete object with invalid MFA with error
mfaHeader := generateMFAHeader(key, deviceName)
deleteObjectWithMFAErr(hc, bktName, objName, ver, mfaHeader+"1", apierr.GetAPIError(apierr.ErrMFAAuthNeeded))
// delete object with MFA successfully
deleteObjectWithMFA(hc, bktName, objName, ver, generateMFAHeader(key, deviceName))
// disable MFA and delete object successfully
objInfo = createTestObject(hc, bktInfo, objName, encryption.Params{})
ver = objInfo.VersionID()
putBucketVersioningMFADelete(hc, bktName, "Enabled", "", generateMFAHeader(key, deviceName))
deleteObjectWithMFA(hc, bktName, objName, ver, generateMFAHeader(key, deviceName))
})
t.Run("versioned bucket without verId", func(t *testing.T) {
hc := prepareHandlerContext(t)
bktInfo := createTestBucket(hc, bktName)
key := createMFADevice(hc, bktName, deviceName)
putBucketVersioningMFADelete(hc, bktName, "Enabled", "Enabled", generateMFAHeader(key, deviceName))
objInfo := createTestObject(hc, bktInfo, objName, encryption.Params{})
deleteObject(t, hc, bktName, objInfo.Name, "")
})
} }
func TestRemovalOnReplace(t *testing.T) { func TestRemovalOnReplace(t *testing.T) {
@ -547,34 +605,67 @@ func createVersionedBucketAndObject(_ *testing.T, tc *handlerContext, bktName, o
func createVersionedBucket(hc *handlerContext, bktName string) *data.BucketInfo { func createVersionedBucket(hc *handlerContext, bktName string) *data.BucketInfo {
bktInfo := createTestBucket(hc, bktName) bktInfo := createTestBucket(hc, bktName)
putBucketVersioning(hc.t, hc, bktName, true, "") putBucketVersioning(hc.t, hc, bktName, "Enabled")
return bktInfo return bktInfo
} }
func createVersionedBucketMFAEnabled(hc *handlerContext, bktName, mfa string) *data.BucketInfo { func createMFADevice(hc *handlerContext, bktName, device string) *otp.Key {
bktInfo := createTestBucket(hc, bktName) otpKey, err := totp.Generate(totp.GenerateOpts{
putBucketVersioning(hc.t, hc, bktName, true, mfa) Issuer: bktName,
AccountName: bktName,
})
require.NoError(hc.t, err)
return bktInfo err = hc.h.mfa.CreateMFADevice(hc.context, mfa.SecretDevice{
Device: *mfa.NewDevice("", device, "/"),
Key: otpKey,
})
require.NoError(hc.t, err)
return otpKey
} }
func putBucketVersioning(t *testing.T, tc *handlerContext, bktName string, enabled bool, mfa string) { func generateMFAHeader(key *otp.Key, device string) string {
cfg := &VersioningConfiguration{Status: "Suspended"} code, _ := totp.GenerateCode(key.Secret(), time.Now().UTC()) // error should never happen with otp.Key
if enabled { return "arn:aws:iam:::mfa/" + device + " " + code
}
func putBucketVersioning(t *testing.T, hc *handlerContext, bktName string, status string) {
w := putBucketVersioningBase(hc, bktName, status, "", "")
assertStatus(t, w, http.StatusOK)
}
func putBucketVersioningMFADelete(hc *handlerContext, bktName string, versioning string, mfa string, mfaHeader string) {
w := putBucketVersioningBase(hc, bktName, versioning, mfa, mfaHeader)
assertStatus(hc.t, w, http.StatusOK)
}
func putBucketVersioningMFADeleteErr(hc *handlerContext, bktName string, versioning string, mfa string, mfaHeader string, err apierr.Error) {
w := putBucketVersioningBase(hc, bktName, versioning, mfa, mfaHeader)
assertS3Error(hc.t, w, err)
}
func putBucketVersioningBase(tc *handlerContext, bktName string, versioning string, mfa string, mfaHeader string) *httptest.ResponseRecorder {
cfg := &VersioningConfiguration{}
switch versioning {
case "Suspended":
cfg.Status = "Suspended"
case "Enabled":
cfg.Status = "Enabled" cfg.Status = "Enabled"
} }
if len(mfa) > 0 { switch mfa {
case "Enabled":
cfg.MfaDelete = "Enabled" cfg.MfaDelete = "Enabled"
case "Disabled":
cfg.MfaDelete = "Disabled"
} }
w, r := prepareTestRequest(tc, bktName, "", cfg) w, r := prepareTestRequest(tc, bktName, "", cfg)
if len(mfaHeader) > 0 {
if len(mfa) > 0 { r.Header.Set(api.AmzMFA, mfaHeader)
r.Header.Set(api.AmzMFA, mfa)
} }
tc.Handler().PutBucketVersioningHandler(w, r) tc.Handler().PutBucketVersioningHandler(w, r)
assertStatus(t, w, http.StatusOK) return w
} }
func getBucketVersioning(hc *handlerContext, bktName string) *VersioningConfiguration { func getBucketVersioning(hc *handlerContext, bktName string) *VersioningConfiguration {
@ -598,6 +689,37 @@ func deleteObject(t *testing.T, tc *handlerContext, bktName, objName, version st
return w.Header().Get(api.AmzVersionID), w.Header().Get(api.AmzDeleteMarker) != "" return w.Header().Get(api.AmzVersionID), w.Header().Get(api.AmzDeleteMarker) != ""
} }
func deleteObjectWithMFA(tc *handlerContext, bktName, objName, version, mfa string) (string, bool) {
query := make(url.Values)
query.Add(api.QueryVersionID, version)
w, r := prepareTestFullRequest(tc, bktName, objName, query, nil)
r.Header.Set(api.AmzMFA, mfa)
tc.Handler().DeleteObjectHandler(w, r)
assertStatus(tc.t, w, http.StatusNoContent)
return w.Header().Get(api.AmzVersionID), w.Header().Get(api.AmzDeleteMarker) != ""
}
func deleteObjectErr(t *testing.T, tc *handlerContext, bktName, objName, version string, err apierr.Error) {
query := make(url.Values)
query.Add(api.QueryVersionID, version)
w, r := prepareTestFullRequest(tc, bktName, objName, query, nil)
tc.Handler().DeleteObjectHandler(w, r)
assertS3Error(t, w, err)
}
func deleteObjectWithMFAErr(tc *handlerContext, bktName, objName, version, mfa string, err apierr.Error) {
query := make(url.Values)
query.Add(api.QueryVersionID, version)
w, r := prepareTestFullRequest(tc, bktName, objName, query, nil)
r.Header.Set(api.AmzMFA, mfa)
tc.Handler().DeleteObjectHandler(w, r)
assertS3Error(tc.t, w, err)
}
func deleteObjects(t *testing.T, tc *handlerContext, bktName string, objVersions [][2]string) *DeleteObjectsResponse { func deleteObjects(t *testing.T, tc *handlerContext, bktName string, objVersions [][2]string) *DeleteObjectsResponse {
w := deleteObjectsBase(tc, bktName, objVersions) w := deleteObjectsBase(tc, bktName, objVersions)
@ -696,6 +818,6 @@ func createSuspendedBucket(t *testing.T, tc *handlerContext, bktName string) *da
createTestBucket(tc, bktName) createTestBucket(tc, bktName)
bktInfo, err := tc.Layer().GetBucketInfo(tc.Context(), bktName) bktInfo, err := tc.Layer().GetBucketInfo(tc.Context(), bktName)
require.NoError(t, err) require.NoError(t, err)
putBucketVersioning(t, tc, bktName, false, "") putBucketVersioning(t, tc, bktName, "Suspended")
return bktInfo return bktInfo
} }

View file

@ -25,6 +25,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
intmfa "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/mfa"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree"
bearertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer/test" bearertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer/test"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
@ -180,6 +181,7 @@ type handlerConfig struct {
cacheCfg *layer.CachesConfig cacheCfg *layer.CachesConfig
withoutCORS bool withoutCORS bool
withoutLifecycle bool withoutLifecycle bool
withoutMFA bool
} }
func prepareHandlerContext(t *testing.T) *handlerContext { func prepareHandlerContext(t *testing.T) *handlerContext {
@ -198,6 +200,7 @@ func prepareWithoutContainersHandlerContext(t *testing.T, cors, lifecycle bool)
cacheCfg: layer.DefaultCachesConfigs(log), cacheCfg: layer.DefaultCachesConfigs(log),
withoutCORS: cors, withoutCORS: cors,
withoutLifecycle: lifecycle, withoutLifecycle: lifecycle,
withoutMFA: true,
}, log) }, log)
require.NoError(t, err) require.NoError(t, err)
return &handlerContext{ return &handlerContext{
@ -289,7 +292,14 @@ func prepareHandlerContextBase(config *handlerConfig, log *zap.Logger) (*handler
cfg: cfg, cfg: cfg,
ape: newAPEMock(), ape: newAPEMock(),
frostfsid: newFrostfsIDMock(), frostfsid: newFrostfsIDMock(),
mfa: newMFAMock(), }
if !config.withoutMFA {
// this code creates one more container, so it may affect tests
h.mfa, err = newMFAMock(log, layerCfg.GateKey, tp, memCli)
if err != nil {
return nil, err
}
} }
accessBox, err := newTestAccessBox(key) accessBox, err := newTestAccessBox(key)
@ -483,70 +493,45 @@ func newFrostfsIDMock() *frostfsidMock {
return &frostfsidMock{data: map[string]*keys.PublicKey{}} return &frostfsidMock{data: map[string]*keys.PublicKey{}}
} }
func newMFAMock() *mfa.Manager { type unlocker struct {
cfg := mfa.Config{ k *keys.PrivateKey
Storage: newStorageMock(), }
Unlocker: nil,
Container: cid.ID{}, func (u unlocker) PrivateKey() *keys.PrivateKey {
Logger: nil, return u.k
}
func (u unlocker) PublicKeys() []*keys.PublicKey {
return []*keys.PublicKey{
u.k.PublicKey(),
}
}
func newMFAMock(log *zap.Logger, key *keys.PrivateKey, p frostfs.FrostFS, t tree.ServiceClient) (*mfa.Manager, error) {
bktName := "mfa"
res, err := p.CreateContainer(context.Background(), frostfs.PrmContainerCreate{
Name: bktName,
Policy: getPlacementPolicy(),
})
if err != nil {
return nil, err
} }
manager, _ := mfa.NewManager(cfg) f := intmfa.NewMFAFrostFS(intmfa.FrostFSMFAConfig{
ObjStor: p,
TreeStor: t,
Key: key,
Logger: log,
})
return man cfg := mfa.Config{
} Storage: f,
Unlocker: unlocker{k: key},
Container: res.ContainerID,
Logger: log,
}
type man mfa.Manager return mfa.NewManager(cfg)
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) { func (f *frostfsidMock) GetUserAddress(account, user string) (string, error) {

View file

@ -94,6 +94,17 @@ func (h *handler) PutBucketLifecycleHandler(w http.ResponseWriter, r *http.Reque
return return
} }
bktSettings, err := h.obj.GetBucketSettings(ctx, bktInfo)
if err != nil {
h.logAndSendError(ctx, w, "could not get bucket settings", reqInfo, err)
return
}
if bktSettings.MFADeleteEnabled() {
h.logAndSendError(ctx, w, "failed to add", reqInfo, apierr.GetAPIError(apierr.ErrCannotPutLifecycleConfiguration))
return
}
networkInfo, err := h.obj.GetNetworkInfo(ctx) networkInfo, err := h.obj.GetNetworkInfo(ctx)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "could not get network info", reqInfo, err) h.logAndSendError(ctx, w, "could not get network info", reqInfo, err)

View file

@ -521,6 +521,42 @@ func TestPutBucketLifecycleInvalidXML(t *testing.T) {
assertS3Error(hc.t, w, apierr.GetAPIError(apierr.ErrMalformedXML)) assertS3Error(hc.t, w, apierr.GetAPIError(apierr.ErrMalformedXML))
} }
func TestPutMFADeleteWithLifecycleBucket(t *testing.T) {
hc := prepareHandlerContext(t)
lifecycle := &data.LifecycleConfiguration{
Rules: []data.LifecycleRule{
{
Status: data.LifecycleStatusEnabled,
Expiration: &data.LifecycleExpiration{
Days: ptr(21),
},
},
},
}
// S3 Lifecycle configuration on multi-factor authentication (MFA)-enabled buckets isn't supported.
t.Run("lifecycle in MFA Delete bucket", func(_ *testing.T) {
bktName := "mfa-delete-bucket"
deviceName := "device"
createBucket(hc, bktName)
key := createMFADevice(hc, bktName, deviceName)
putBucketVersioningMFADelete(hc, bktName, "Enabled", "Enabled", generateMFAHeader(key, deviceName))
putBucketLifecycleConfigurationErr(hc, bktName, lifecycle, nil, apierr.GetAPIError(apierr.ErrCannotPutLifecycleConfiguration))
})
// You cannot use MFA delete with lifecycle configurations.
t.Run("MFA Delete in lifecycle bucket", func(_ *testing.T) {
bktName := "lifecycle-bucket"
deviceName := "device2"
createBucket(hc, bktName)
key := createMFADevice(hc, bktName, deviceName)
putBucketLifecycleConfiguration(hc, bktName, lifecycle, nil, false)
putBucketVersioningMFADeleteErr(hc, bktName, "Enabled", "Enabled", generateMFAHeader(key, deviceName), apierr.GetAPIError(apierr.ErrMFAAuthIsNotSupported))
})
}
func TestPutBucketLifecycleCopiesNumbers(t *testing.T) { func TestPutBucketLifecycleCopiesNumbers(t *testing.T) {
t.Run("with lifecycle container", func(t *testing.T) { t.Run("with lifecycle container", func(t *testing.T) {
hc := prepareHandlerContext(t) hc := prepareHandlerContext(t)

View file

@ -69,7 +69,7 @@ func TestDeleteMultipartAllParts(t *testing.T) {
// versions bucket // versions bucket
createTestBucket(hc, bktName2) createTestBucket(hc, bktName2)
putBucketVersioning(t, hc, bktName2, true, "") putBucketVersioning(t, hc, bktName2, "Enabled")
multipartUpload(hc, bktName2, objName, nil, objLen, partSize) multipartUpload(hc, bktName2, objName, nil, objLen, partSize)
_, hdr := getObject(hc, bktName2, objName) _, hdr := getObject(hc, bktName2, objName)
versionID := hdr.Get("X-Amz-Version-Id") versionID := hdr.Get("X-Amz-Version-Id")
@ -107,7 +107,7 @@ func TestSpecialMultipartName(t *testing.T) {
bktName, objName := "bucket", "bucket-settings" bktName, objName := "bucket", "bucket-settings"
createTestBucket(hc, bktName) createTestBucket(hc, bktName)
putBucketVersioning(t, hc, bktName, true, "") putBucketVersioning(t, hc, bktName, "Enabled")
createMultipartUpload(hc, bktName, objName, nil) createMultipartUpload(hc, bktName, objName, nil)
res := getBucketVersioning(hc, bktName) res := getBucketVersioning(hc, bktName)

View file

@ -64,7 +64,7 @@ func TestListObjectNullVersions(t *testing.T) {
createTestBucket(hc, bktName) createTestBucket(hc, bktName)
putObjectContent(hc, bktName, objName, "content") putObjectContent(hc, bktName, objName, "content")
putBucketVersioning(t, hc, bktName, true, "") putBucketVersioning(t, hc, bktName, "Enabled")
putObjectContent(hc, bktName, objName, "content2") putObjectContent(hc, bktName, objName, "content2")
result := listVersions(t, hc, bktName) result := listVersions(t, hc, bktName)
@ -226,7 +226,7 @@ func TestListObjectsLatestVersions(t *testing.T) {
bktName := "bucket-versioning-enabled" bktName := "bucket-versioning-enabled"
createTestBucket(hc, bktName) createTestBucket(hc, bktName)
putBucketVersioning(t, hc, bktName, true, "") putBucketVersioning(t, hc, bktName, "Enabled")
objName1, objName2 := "object1", "object2" objName1, objName2 := "object1", "object2"
objContent1, objContent2 := "content1", "content2" objContent1, objContent2 := "content1", "content2"
@ -762,7 +762,7 @@ func TestMintVersioningListObjectVersionsVersionIDContinuation(t *testing.T) {
bktName, objName := "mint-bucket-for-listing-versions", "objName" bktName, objName := "mint-bucket-for-listing-versions", "objName"
createTestBucket(hc, bktName) createTestBucket(hc, bktName)
putBucketVersioning(t, hc, bktName, true, "") putBucketVersioning(t, hc, bktName, "Enabled")
length := 10 length := 10
objects := make([]string, length) objects := make([]string, length)
@ -795,7 +795,7 @@ func TestListObjectVersionsEncoding(t *testing.T) {
bktName := "bucket-for-listing-versions-encoding" bktName := "bucket-for-listing-versions-encoding"
bktInfo := createTestBucket(hc, bktName) bktInfo := createTestBucket(hc, bktName)
putBucketVersioning(t, hc, bktName, true, "") putBucketVersioning(t, hc, bktName, "Enabled")
objects := []string{"foo()/bar", "foo()/bar/xyzzy", "auux ab/thud", "asdf+b"} objects := []string{"foo()/bar", "foo()/bar/xyzzy", "auux ab/thud", "asdf+b"}
for _, objName := range objects { for _, objName := range objects {

View file

@ -170,6 +170,9 @@ func parseRange(s string) (*layer.RangeParams, error) {
} }
func nameFromArn(arn string) string { func nameFromArn(arn string) string {
if len(arn) == 0 {
return ""
}
pts := strings.Split(arn, "/") pts := strings.Split(arn, "/")
return pts[len(pts)-1] return pts[len(pts)-1]
} }

View file

@ -24,6 +24,12 @@ func (h *handler) PutBucketVersioningHandler(w http.ResponseWriter, r *http.Requ
h.logAndSendError(ctx, w, "couldn't decode versioning configuration", reqInfo, errors.GetAPIError(errors.ErrIllegalVersioningConfigurationException)) h.logAndSendError(ctx, w, "couldn't decode versioning configuration", reqInfo, errors.GetAPIError(errors.ErrIllegalVersioningConfigurationException))
return return
} }
newMfa := len(configuration.MfaDelete) > 0
newStatus := len(configuration.Status) > 0
if !newStatus {
h.logAndSendError(ctx, w, "failed to put versioning", reqInfo, errors.GetAPIError(errors.ErrVersioningNotSpecified))
}
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
if err != nil { if err != nil {
@ -37,12 +43,21 @@ func (h *handler) PutBucketVersioningHandler(w http.ResponseWriter, r *http.Requ
return return
} }
newMfa := len(configuration.MfaDelete) > 0 lifecycleCfg, err := h.obj.GetBucketLifecycleConfiguration(ctx, bktInfo)
if err != nil && !errors.IsS3Error(err, errors.ErrNoSuchLifecycleConfiguration) {
h.logAndSendError(ctx, w, "couldn't get lifecycle config", reqInfo, err)
return
}
if lifecycleCfg != nil && newMfa {
h.logAndSendError(ctx, w, "couldn't put versioning", reqInfo, errors.GetAPIError(errors.ErrMFAAuthIsNotSupported))
return
}
if settings.MFADeleteEnabled() || newMfa { if settings.MFADeleteEnabled() || newMfa {
serialNumber, token, err = h.getMFAHeader(r) serialNumber, token, err = h.getMFAHeader(r)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "invalid x-amz-mfa header", reqInfo, errors.GetAPIError(errors.ErrBadRequest)) h.logAndSendError(ctx, w, "invalid x-amz-mfa header", reqInfo, errors.GetAPIError(errors.ErrInvalidMFAHeader))
return return
} }
device, err := h.mfa.GetMFADevice(ctx, reqInfo.Namespace, nameFromArn(serialNumber)) device, err := h.mfa.GetMFADevice(ctx, reqInfo.Namespace, nameFromArn(serialNumber))
@ -68,6 +83,7 @@ func (h *handler) PutBucketVersioningHandler(w http.ResponseWriter, r *http.Requ
newSettings.Versioning.MFASerialNumber = serialNumber newSettings.Versioning.MFASerialNumber = serialNumber
case data.MFADeleteDisabled: case data.MFADeleteDisabled:
newSettings.Versioning.MFADeleteStatus = data.MFADeleteDisabled newSettings.Versioning.MFADeleteStatus = data.MFADeleteDisabled
newSettings.Versioning.MFASerialNumber = ""
default: default:
h.logAndSendError(ctx, w, "failed to get mfa configuration", reqInfo, nil) h.logAndSendError(ctx, w, "failed to get mfa configuration", reqInfo, nil)
return return
@ -127,6 +143,8 @@ func formVersioningConfiguration(settings *data.BucketSettings) *VersioningConfi
} }
if settings.MFADeleteEnabled() { if settings.MFADeleteEnabled() {
res.MfaDelete = data.MFADeleteEnabled res.MfaDelete = data.MFADeleteEnabled
} else if settings.MFADeleteDisabled() {
res.MfaDelete = data.MFADeleteDisabled
} }
return res return res

View file

@ -0,0 +1,45 @@
package handler
import (
"testing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"github.com/stretchr/testify/require"
)
func TestVersioningWithMFADelete(t *testing.T) {
bktName := "bucket-name"
deviceName := "device"
hc := prepareHandlerContext(t)
createBucket(hc, bktName)
key := createMFADevice(hc, bktName, deviceName)
putBucketVersioningMFADeleteErr(hc, bktName, "", "Enabled", generateMFAHeader(key, deviceName), errors.GetAPIError(errors.ErrVersioningNotSpecified))
putBucketVersioningMFADeleteErr(hc, bktName, "", "", "", errors.GetAPIError(errors.ErrVersioningNotSpecified))
// set MFA Delete status
putBucketVersioningMFADelete(hc, bktName, "Suspended", "Enabled", generateMFAHeader(key, deviceName))
require.Equal(t, "Enabled", getBucketVersioning(hc, bktName).MfaDelete)
require.Equal(t, "Suspended", getBucketVersioning(hc, bktName).Status)
// try to change versioning without MFA Header
putBucketVersioningMFADeleteErr(hc, bktName, "Enabled", "Enabled", "", errors.GetAPIError(errors.ErrInvalidMFAHeader))
require.Equal(t, "Enabled", getBucketVersioning(hc, bktName).MfaDelete)
require.Equal(t, "Suspended", getBucketVersioning(hc, bktName).Status)
// change versioning with MFA successfully
putBucketVersioningMFADelete(hc, bktName, "Enabled", "Enabled", generateMFAHeader(key, deviceName))
require.Equal(t, "Enabled", getBucketVersioning(hc, bktName).MfaDelete)
require.Equal(t, "Enabled", getBucketVersioning(hc, bktName).Status)
// try to disable MFA without MFA Header
putBucketVersioningMFADeleteErr(hc, bktName, "Enabled", "Disabled", "", errors.GetAPIError(errors.ErrInvalidMFAHeader))
require.Equal(t, "Enabled", getBucketVersioning(hc, bktName).MfaDelete)
require.Equal(t, "Enabled", getBucketVersioning(hc, bktName).Status)
// try to disable MFA successfully
putBucketVersioningMFADelete(hc, bktName, "Enabled", "Disabled", generateMFAHeader(key, deviceName))
require.Equal(t, "Disabled", getBucketVersioning(hc, bktName).MfaDelete)
require.Equal(t, "Enabled", getBucketVersioning(hc, bktName).Status)
}

View file

@ -302,7 +302,6 @@ func TestVersioningDeleteObject(t *testing.T) {
tc := prepareContext(t) tc := prepareContext(t)
settings := &data.BucketSettings{Versioning: data.Versioning{ settings := &data.BucketSettings{Versioning: data.Versioning{
VersioningStatus: data.VersioningEnabled, VersioningStatus: data.VersioningEnabled,
MFADeleteStatus: data.MFADeleteDisabled,
}} }}
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{ err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
BktInfo: tc.bktInfo, BktInfo: tc.bktInfo,
@ -327,7 +326,6 @@ func TestGetUnversioned(t *testing.T) {
settings := &data.BucketSettings{Versioning: data.Versioning{ settings := &data.BucketSettings{Versioning: data.Versioning{
VersioningStatus: data.VersioningUnversioned, VersioningStatus: data.VersioningUnversioned,
MFADeleteStatus: data.MFADeleteDisabled,
}} }}
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{ err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
BktInfo: tc.bktInfo, BktInfo: tc.bktInfo,
@ -344,7 +342,6 @@ func TestVersioningDeleteSpecificObjectVersion(t *testing.T) {
tc := prepareContext(t) tc := prepareContext(t)
settings := &data.BucketSettings{Versioning: data.Versioning{ settings := &data.BucketSettings{Versioning: data.Versioning{
VersioningStatus: data.VersioningEnabled, VersioningStatus: data.VersioningEnabled,
MFADeleteStatus: data.MFADeleteDisabled,
}} }}
err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{ err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{
BktInfo: tc.bktInfo, BktInfo: tc.bktInfo,

View file

@ -35,6 +35,7 @@ import (
containerClient "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/container" containerClient "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/container"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid"
ffidcontract "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid/contract" ffidcontract "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid/contract"
intmfa "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/mfa"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/policy" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/policy"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/policy/contract" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/policy/contract"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/services" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/services"
@ -80,8 +81,6 @@ type (
frostfsid *frostfsid.FrostFSID frostfsid *frostfsid.FrostFSID
mfaManager *mfa.Manager
policyStorage *policy.Storage policyStorage *policy.Storage
servers []Server servers []Server
@ -258,7 +257,6 @@ func (a *App) init(ctx context.Context) {
a.initFrostfsID(rpcCli) a.initFrostfsID(rpcCli)
a.initPolicyStorage(rpcCli) a.initPolicyStorage(rpcCli)
a.initAPI(ctx, rpcCli) a.initAPI(ctx, rpcCli)
a.initMfaManager(ctx)
a.initMetrics() a.initMetrics()
a.initServers(ctx) a.initServers(ctx)
a.initTracing(ctx) a.initTracing(ctx)
@ -722,7 +720,8 @@ func (s *appSettings) LifecycleCopiesNumbers() []uint32 {
func (a *App) initAPI(ctx context.Context, rpcCli *rpcclient.Client) { func (a *App) initAPI(ctx context.Context, rpcCli *rpcclient.Client) {
a.initLayer(ctx, rpcCli) a.initLayer(ctx, rpcCli)
a.initHandler()
a.initHandler(ctx)
} }
func (a *App) initMetrics() { func (a *App) initMetrics() {
@ -1271,7 +1270,7 @@ func getFrostfsIDCacheConfig(v *viper.Viper, l *zap.Logger) *cache.Config {
return cacheCfg return cacheCfg
} }
func (a *App) initMfaManager(ctx context.Context) { func (a *App) initMfaManager(ctx context.Context) *mfa.Manager {
var err error var err error
var mfaCnrInfo *data.BucketInfo var mfaCnrInfo *data.BucketInfo
@ -1280,33 +1279,31 @@ func (a *App) initMfaManager(ctx context.Context) {
if err != nil { if err != nil {
a.log.Fatal(logs.CouldNotFetchMFAContainerInfo, zap.Error(err), logs.TagField(logs.TagApp)) a.log.Fatal(logs.CouldNotFetchMFAContainerInfo, zap.Error(err), logs.TagField(logs.TagApp))
} }
} else {
a.log.Fatal(logs.CouldNotFetchMFAContainerInfo, logs.TagField(logs.TagApp))
} }
mfaConfig, err := a.fetchMFAConfig(mfaCnrInfo.CID) mfaConfig := a.fetchMFAConfig(mfaCnrInfo.CID)
if err != nil {
a.log.Fatal(logs.CouldNotInitMFAClient, zap.Error(err), logs.TagField(logs.TagApp))
}
manager, err := mfa.NewManager(mfaConfig) manager, err := mfa.NewManager(mfaConfig)
if err != nil { if err != nil {
a.log.Fatal(logs.CouldNotInitMFAClient, zap.Error(err), logs.TagField(logs.TagApp)) a.log.Fatal(logs.CouldNotInitMFAClient, zap.Error(err), logs.TagField(logs.TagApp))
} }
a.mfaManager = manager return manager
} }
func (a *App) initHandler() { func (a *App) initHandler(ctx context.Context) {
var err error var err error
manager := a.initMfaManager(ctx)
a.api, err = handler.New(a.log, a.obj, a.settings, a.policyStorage, a.frostfsid, a.mfaManager) a.api, err = handler.New(a.log, a.obj, a.settings, a.policyStorage, a.frostfsid, manager)
if err != nil { if err != nil {
a.log.Fatal(logs.CouldNotInitializeAPIHandler, zap.Error(err), logs.TagField(logs.TagApp)) a.log.Fatal(logs.CouldNotInitializeAPIHandler, zap.Error(err), logs.TagField(logs.TagApp))
} }
} }
func (a *App) fetchMFAConfig(id cid.ID) (mfa.Config, error) { func (a *App) fetchMFAConfig(id cid.ID) mfa.Config {
mfaFrostFS := frostfs.NewMFAFrostFS(frostfs.MFAFrostFSConfig{ mfaFrostFS := intmfa.NewMFAFrostFS(intmfa.FrostFSMFAConfig{
Pool: a.pool, ObjStor: frostfs.NewFrostFS(a.pool, a.key),
TreePool: a.treePool, TreeStor: services.NewPoolWrapper(a.treePool),
Key: a.key, Key: a.key,
Logger: a.log, Logger: a.log,
}) })
@ -1318,7 +1315,7 @@ func (a *App) fetchMFAConfig(id cid.ID) (mfa.Config, error) {
Logger: a.log, Logger: a.log,
} }
return config, nil return config
} }
func (a *App) getServer(address string) Server { func (a *App) getServer(address string) Server {

View file

@ -1,4 +1,4 @@
package frostfs package mfa
import ( import (
"bytes" "bytes"
@ -9,37 +9,41 @@ import (
"strings" "strings"
"git.frostfs.info/TrueCloudLab/frostfs-mfa/mfa" "git.frostfs.info/TrueCloudLab/frostfs-mfa/mfa"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs" "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/api/middleware"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
apitree "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/tree" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/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" treepool "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool/tree"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"go.uber.org/zap" "go.uber.org/zap"
) )
// MFAFrostFS is a mediator which implements mfa.Storage through pool.Pool and treepool.Pool. // FrostFSMFA is a mediator which implements mfa.Storage through pool.Pool and treepool.Pool.
type MFAFrostFS struct { type FrostFSMFA struct {
frostFS *FrostFS objStor frostfs.FrostFS
treePool *treepool.Pool treeStor tree.ServiceClient
log *zap.Logger log *zap.Logger
key *keys.PrivateKey key *keys.PrivateKey
} }
func (m *MFAFrostFS) PrivateKey() *keys.PrivateKey { var (
_ mfa.Storage = (*FrostFSMFA)(nil)
_ mfa.KeyStore = (*FrostFSMFA)(nil)
)
func (m *FrostFSMFA) PrivateKey() *keys.PrivateKey {
return m.key return m.key
} }
func (m *MFAFrostFS) PublicKeys() []*keys.PublicKey { func (m *FrostFSMFA) PublicKeys() []*keys.PublicKey {
// TODO implement me
panic("Readonly MFA manager should not call PublicKeys()") panic("Readonly MFA manager should not call PublicKeys()")
} }
func (m *MFAFrostFS) CreateObject(ctx context.Context, create mfa.PrmObjectCreate) (oid.ID, error) { func (m *FrostFSMFA) CreateObject(ctx context.Context, create mfa.PrmObjectCreate) (oid.ID, error) {
object, err := m.frostFS.CreateObject(ctx, frostfs.PrmObjectCreate{ object, err := m.objStor.CreateObject(ctx, frostfs.PrmObjectCreate{
Container: create.Container, Container: create.Container,
Payload: bytes.NewReader(create.Payload), Payload: bytes.NewReader(create.Payload),
Filepath: create.FilePath, Filepath: create.FilePath,
@ -47,21 +51,21 @@ func (m *MFAFrostFS) CreateObject(ctx context.Context, create mfa.PrmObjectCreat
WithoutHomomorphicHash: true, WithoutHomomorphicHash: true,
}) })
if err != nil { if err != nil {
return [32]byte{}, err return oid.ID{}, err
} }
return object.ObjectID, nil return object.ObjectID, nil
} }
func (m *MFAFrostFS) DeleteObject(ctx context.Context, address oid.Address) error { func (m *FrostFSMFA) DeleteObject(ctx context.Context, address oid.Address) error {
prm := frostfs.PrmObjectDelete{ prm := frostfs.PrmObjectDelete{
Container: address.Container(), Container: address.Container(),
Object: address.Object(), Object: address.Object(),
} }
return m.frostFS.DeleteObject(ctx, prm) return m.objStor.DeleteObject(ctx, prm)
} }
func (m *MFAFrostFS) SetTreeNode(ctx context.Context, cnrID cid.ID, name string, meta map[string]string) (*mfa.TreeMultiNode, error) { func (m *FrostFSMFA) SetTreeNode(ctx context.Context, cnrID cid.ID, name string, meta map[string]string) (*mfa.TreeMultiNode, error) {
if len(name) == 0 { if len(name) == 0 {
return nil, errors.New("tree node name must not be empty") return nil, errors.New("tree node name must not be empty")
} }
@ -76,15 +80,7 @@ func (m *MFAFrostFS) SetTreeNode(ctx context.Context, cnrID cid.ID, name string,
} }
if isErrNotFound { if isErrNotFound {
prmAdd := treepool.AddNodeByPathParams{ if _, err = m.treeStor.AddNodeByPath(ctx, &data.BucketInfo{CID: cnrID}, mfaTreeName, path[:len(path)-1], meta); err != nil {
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 nil, fmt.Errorf("add node by path: %w", err)
} }
@ -92,15 +88,7 @@ func (m *MFAFrostFS) SetTreeNode(ctx context.Context, cnrID cid.ID, name string,
} }
node := multiNode.Latest() node := multiNode.Latest()
prmMove := treepool.MoveNodeParams{ if err = m.treeStor.MoveNode(ctx, &data.BucketInfo{CID: cnrID}, mfaTreeName, node.ID, node.ParentID, meta); err != nil {
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) return nil, fmt.Errorf("move node: %w", err)
} }
@ -112,7 +100,7 @@ func (m *MFAFrostFS) SetTreeNode(ctx context.Context, cnrID cid.ID, name string,
return mfaMultiNode, nil return mfaMultiNode, nil
} }
func (m *MFAFrostFS) GetTreeNode(ctx context.Context, cnrID cid.ID, name string) (*mfa.TreeMultiNode, error) { func (m *FrostFSMFA) GetTreeNode(ctx context.Context, cnrID cid.ID, name string) (*mfa.TreeMultiNode, error) {
multiNode, err := m.getTreeNode(ctx, cnrID, pathFromName(name)) multiNode, err := m.getTreeNode(ctx, cnrID, pathFromName(name))
if err != nil { if err != nil {
return nil, fmt.Errorf("couldn't get node: %w", err) return nil, fmt.Errorf("couldn't get node: %w", err)
@ -121,7 +109,7 @@ func (m *MFAFrostFS) GetTreeNode(ctx context.Context, cnrID cid.ID, name string)
return multiNode.ToMFAMultiNode(), nil return multiNode.ToMFAMultiNode(), nil
} }
func (m *MFAFrostFS) DeleteTreeNode(ctx context.Context, cnrID cid.ID, name string) ([]*mfa.TreeNode, error) { func (m *FrostFSMFA) DeleteTreeNode(ctx context.Context, cnrID cid.ID, name string) ([]*mfa.TreeNode, error) {
multiNode, err := m.getTreeNode(ctx, cnrID, pathFromName(name)) multiNode, err := m.getTreeNode(ctx, cnrID, pathFromName(name))
if err != nil { if err != nil {
return nil, fmt.Errorf("couldn't get node: %w", err) return nil, fmt.Errorf("couldn't get node: %w", err)
@ -139,7 +127,7 @@ func (m *MFAFrostFS) DeleteTreeNode(ctx context.Context, cnrID cid.ID, name stri
return res, nil return res, nil
} }
func (m *MFAFrostFS) GetTreeNodes(ctx context.Context, cnrID cid.ID, prefix string) ([]*mfa.TreeNode, error) { func (m *FrostFSMFA) GetTreeNodes(ctx context.Context, cnrID cid.ID, prefix string) ([]*mfa.TreeNode, error) {
rootID := []uint64{0} rootID := []uint64{0}
if len(prefix) != 0 { if len(prefix) != 0 {
var err error var err error
@ -152,18 +140,7 @@ func (m *MFAFrostFS) GetTreeNodes(ctx context.Context, cnrID cid.ID, prefix stri
} }
} }
prm := treepool.GetSubTreeParams{ allNodes, err := m.treeStor.GetSubTree(ctx, &data.BucketInfo{CID: cnrID}, mfaTreeName, rootID, 0, false)
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 { if err != nil {
return nil, err return nil, err
} }
@ -184,8 +161,8 @@ func (m *MFAFrostFS) GetTreeNodes(ctx context.Context, cnrID cid.ID, prefix stri
return res, nil return res, nil
} }
func filterUnique(allNodes []*apitree.GetSubTreeResponseBody) map[string]*apitree.GetSubTreeResponseBody { func filterUnique(allNodes []tree.NodeResponse) map[string]tree.NodeResponse {
res := make(map[string]*apitree.GetSubTreeResponseBody, len(allNodes)) res := make(map[string]tree.NodeResponse, len(allNodes))
for _, node := range allNodes { for _, node := range allNodes {
var name string var name string
for _, kv := range node.GetMeta() { for _, kv := range node.GetMeta() {
@ -215,9 +192,9 @@ func getMaxTimestamp(timestamps []uint64) uint64 {
return maxTimestamp return maxTimestamp
} }
type MFAFrostFSConfig struct { type FrostFSMFAConfig struct {
Pool *pool.Pool ObjStor frostfs.FrostFS
TreePool *treepool.Pool TreeStor tree.ServiceClient
Key *keys.PrivateKey Key *keys.PrivateKey
Logger *zap.Logger Logger *zap.Logger
} }
@ -240,17 +217,17 @@ const (
) )
// NewMFAFrostFS creates new MFAFrostFS using provided pool.Pool. // NewMFAFrostFS creates new MFAFrostFS using provided pool.Pool.
func NewMFAFrostFS(cfg MFAFrostFSConfig) *MFAFrostFS { func NewMFAFrostFS(cfg FrostFSMFAConfig) *FrostFSMFA {
return &MFAFrostFS{ return &FrostFSMFA{
frostFS: NewFrostFS(cfg.Pool, cfg.Key), objStor: cfg.ObjStor,
treePool: cfg.TreePool, treeStor: cfg.TreeStor,
log: cfg.Logger, log: cfg.Logger,
key: cfg.Key, key: cfg.Key,
} }
} }
func (m *MFAFrostFS) GetObject(ctx context.Context, addr oid.Address) ([]byte, error) { func (m *FrostFSMFA) GetObject(ctx context.Context, addr oid.Address) ([]byte, error) {
res, err := m.frostFS.GetObject(ctx, frostfs.PrmObjectGet{ res, err := m.objStor.GetObject(ctx, frostfs.PrmObjectGet{
Container: addr.Container(), Container: addr.Container(),
Object: addr.Object(), Object: addr.Object(),
}) })
@ -267,21 +244,19 @@ func (m *MFAFrostFS) GetObject(ctx context.Context, addr oid.Address) ([]byte, e
return io.ReadAll(res.Payload) return io.ReadAll(res.Payload)
} }
func (m *MFAFrostFS) getTreeNode(ctx context.Context, cnrID cid.ID, path []string) (*multiSystemNode, error) { func (m *FrostFSMFA) getTreeNode(ctx context.Context, cnrID cid.ID, path []string) (*multiSystemNode, error) {
prmGetNodes := treepool.GetNodesParams{ prmGetNodes := &tree.GetNodesParams{
CID: cnrID, BktInfo: &data.BucketInfo{CID: cnrID},
TreeID: mfaTreeName, TreeID: mfaTreeName,
Path: path, Path: path,
PathAttribute: fileNameKey, LatestOnly: true,
LatestOnly: true, AllAttrs: true,
AllAttrs: true,
} }
nodes, err := m.treePool.GetNodes(ctx, prmGetNodes) nodes, err := m.treeStor.GetNodes(ctx, prmGetNodes)
if err != nil { if err != nil {
if errors.Is(err, treepool.ErrNodeNotFound) { if errors.Is(err, tree.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("%w: %s", mfa.ErrTreeNodeNotFound, err.Error())
} }
return nil, fmt.Errorf("get nodes: %w", err) return nil, fmt.Errorf("get nodes: %w", err)
} }
@ -296,7 +271,7 @@ func (m *MFAFrostFS) getTreeNode(ctx context.Context, cnrID cid.ID, path []strin
return newMultiNode(nodes) return newMultiNode(nodes)
} }
func (m *MFAFrostFS) cleanOldNodes(ctx context.Context, nodes []*treeNode, cnrID cid.ID) []*treeNode { func (m *FrostFSMFA) cleanOldNodes(ctx context.Context, nodes []*treeNode, cnrID cid.ID) []*treeNode {
res := make([]*treeNode, 0, len(nodes)) res := make([]*treeNode, 0, len(nodes))
for _, node := range nodes { for _, node := range nodes {
@ -310,14 +285,8 @@ func (m *MFAFrostFS) cleanOldNodes(ctx context.Context, nodes []*treeNode, cnrID
return res return res
} }
func (m *MFAFrostFS) removeTreeNode(ctx context.Context, cnrID cid.ID, nodeID uint64) error { func (m *FrostFSMFA) removeTreeNode(ctx context.Context, cnrID cid.ID, nodeID uint64) error {
prmRemoveNode := treepool.RemoveNodeParams{ err := m.treeStor.RemoveNode(ctx, &data.BucketInfo{CID: cnrID}, mfaTreeName, nodeID)
CID: cnrID,
TreeID: mfaTreeName,
NodeID: nodeID,
}
err := m.treePool.RemoveNode(ctx, prmRemoveNode)
if err != nil { if err != nil {
if errors.Is(err, treepool.ErrNodeNotFound) { if errors.Is(err, treepool.ErrNodeNotFound) {
return fmt.Errorf("%w: %s", mfa.ErrTreeNodeNotFound, err.Error()) return fmt.Errorf("%w: %s", mfa.ErrTreeNodeNotFound, err.Error())
@ -328,16 +297,16 @@ func (m *MFAFrostFS) removeTreeNode(ctx context.Context, cnrID cid.ID, nodeID ui
return nil return nil
} }
func (m *MFAFrostFS) getPrefixNodeID(ctx context.Context, cnrID cid.ID, prefixPath []string) ([]uint64, error) { func (m *FrostFSMFA) getPrefixNodeID(ctx context.Context, cnrID cid.ID, prefixPath []string) ([]uint64, error) {
p := treepool.GetNodesParams{ p := &tree.GetNodesParams{
CID: cnrID, BktInfo: &data.BucketInfo{CID: cnrID},
TreeID: mfaTreeName, TreeID: mfaTreeName,
Path: prefixPath, Path: prefixPath,
LatestOnly: false, LatestOnly: false,
AllAttrs: true, AllAttrs: true,
} }
nodes, err := m.treePool.GetNodes(ctx, p) nodes, err := m.treeStor.GetNodes(ctx, p)
if err != nil { if err != nil {
if errors.Is(err, treepool.ErrNodeNotFound) { if errors.Is(err, treepool.ErrNodeNotFound) {
return nil, fmt.Errorf("%w: %s", mfa.ErrTreeNodeNotFound, err.Error()) return nil, fmt.Errorf("%w: %s", mfa.ErrTreeNodeNotFound, err.Error())
@ -348,7 +317,7 @@ func (m *MFAFrostFS) getPrefixNodeID(ctx context.Context, cnrID cid.ID, prefixPa
var intermediateNodes []uint64 var intermediateNodes []uint64
for _, node := range nodes { for _, node := range nodes {
if isIntermediate(node.GetMeta()) { if isIntermediate(node.GetMeta()) {
intermediateNodes = append(intermediateNodes, node.GetNodeID()) intermediateNodes = append(intermediateNodes, node.GetNodeID()...)
} }
} }
@ -359,7 +328,7 @@ func (m *MFAFrostFS) getPrefixNodeID(ctx context.Context, cnrID cid.ID, prefixPa
return intermediateNodes, nil return intermediateNodes, nil
} }
func isIntermediate(meta []*apitree.KeyValue) bool { func isIntermediate(meta []tree.Meta) bool {
if len(meta) != 1 { if len(meta) != 1 {
return false return false
} }
@ -367,9 +336,8 @@ func isIntermediate(meta []*apitree.KeyValue) bool {
return meta[0].GetKey() == fileNameKey return meta[0].GetKey() == fileNameKey
} }
func newMultiNode(nodes []*apitree.GetNodeByPathResponseInfo) (*multiSystemNode, error) { func newMultiNode(nodes []tree.NodeResponse) (*multiSystemNode, error) {
var ( var (
err error
index int index int
maxTimestamp uint64 maxTimestamp uint64
) )
@ -381,13 +349,22 @@ func newMultiNode(nodes []*apitree.GetNodeByPathResponseInfo) (*multiSystemNode,
treeNodes := make([]*treeNode, len(nodes)) treeNodes := make([]*treeNode, len(nodes))
for i, node := range nodes { for i, node := range nodes {
if treeNodes[i] = newTreeNode(node); err != nil { if len(node.GetTimestamp()) == 0 || len(node.GetNodeID()) == 0 || len(node.GetParentID()) == 0 {
return nil, fmt.Errorf("parse tree node response: %w", err) // this should never happen when GetNodes returns response.
return nil, fmt.Errorf("parse tree node response: %v", node)
} }
treeNodes[i] = &treeNode{
if maxTimestamp < node.GetTimestamp() { ID: node.GetNodeID()[0],
ParentID: node.GetParentID()[0],
TimeStamp: node.GetTimestamp()[0],
Meta: make(map[string]string, len(node.GetMeta())),
}
for _, kv := range node.GetMeta() {
treeNodes[i].Meta[kv.GetKey()] = string(kv.GetValue())
}
if maxTimestamp < node.GetTimestamp()[0] {
index = i index = i
maxTimestamp = node.GetTimestamp() maxTimestamp = node.GetTimestamp()[0]
} }
} }
@ -411,21 +388,6 @@ func (m *multiSystemNode) ToMFAMultiNode() *mfa.TreeMultiNode {
return res 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 { func (m *multiSystemNode) Old() []*treeNode {
return m.nodes[1:] return m.nodes[1:]
} }