diff --git a/api/handler/copy.go b/api/handler/copy.go index 28375399..0970da69 100644 --- a/api/handler/copy.go +++ b/api/handler/copy.go @@ -119,7 +119,8 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) { return } - if err = formObjectLock(params.Lock, bktInfo, settings.LockConfiguration, r.Header); err != nil { + params.Lock, err = formObjectLock(bktInfo, settings.LockConfiguration, r.Header) + if err != nil { h.logAndSendError(w, "could not form object lock", reqInfo, err) return } diff --git a/api/handler/handlers_test.go b/api/handler/handlers_test.go new file mode 100644 index 00000000..495c2683 --- /dev/null +++ b/api/handler/handlers_test.go @@ -0,0 +1,139 @@ +package handler + +import ( + "bytes" + "context" + "encoding/xml" + "fmt" + "math/rand" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/nspcc-dev/neofs-s3-gw/api" + + "github.com/nspcc-dev/neofs-s3-gw/api/data" + + "github.com/nspcc-dev/neofs-s3-gw/api/resolver" + "github.com/nspcc-dev/neofs-sdk-go/container" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neofs-s3-gw/api/layer" + "github.com/nspcc-dev/neofs-s3-gw/api/mock" + "github.com/nspcc-dev/neofs-sdk-go/logger" + "github.com/stretchr/testify/require" +) + +type handlerContext struct { + h *handler + tp *mock.TestPool +} + +func (hc *handlerContext) Handler() *handler { + return hc.h +} + +func (hc *handlerContext) MockedPool() *mock.TestPool { + return hc.tp +} + +func (hc *handlerContext) Layer() layer.Client { + return hc.h.obj +} + +func prepareHandlerContext(t *testing.T) *handlerContext { + key, err := keys.NewPrivateKey() + require.NoError(t, err) + + l, err := logger.New(logger.WithTraceLevel("panic")) + require.NoError(t, err) + tp := mock.NewTestPool() + + testResolver := &resolver.BucketResolver{Name: "test_resolver"} + testResolver.SetResolveFunc(func(ctx context.Context, name string) (*cid.ID, error) { + for id, cnr := range tp.Containers { + for _, attr := range cnr.Attributes() { + if attr.Key() == container.AttributeName && attr.Value() == name { + cnrID := cid.New() + return cnrID, cnrID.Parse(id) + } + } + } + return nil, fmt.Errorf("couldn't resolve container name") + }) + + layerCfg := &layer.Config{ + Caches: layer.DefaultCachesConfigs(), + AnonKey: layer.AnonymousKey{Key: key}, + Resolver: testResolver, + } + + h := &handler{ + log: l, + obj: layer.NewLayer(l, tp, layerCfg), + cfg: &Config{}, + } + + return &handlerContext{ + h: h, + tp: tp, + } +} + +func createTestBucket(ctx context.Context, t *testing.T, h *handlerContext, bktName string) { + cnr := container.New(container.WithAttribute(container.AttributeName, bktName)) + _, err := h.MockedPool().PutContainer(ctx, cnr) + require.NoError(t, err) +} + +func createTestBucketWithLock(ctx context.Context, t *testing.T, h *handlerContext, bktName string, conf *data.ObjectLockConfiguration) { + cnr := container.New(container.WithAttribute(container.AttributeName, bktName), + container.WithAttribute(layer.AttributeLockEnabled, strconv.FormatBool(true))) + cnrID, err := h.MockedPool().PutContainer(ctx, cnr) + require.NoError(t, err) + + sp := &layer.PutSettingsParams{ + BktInfo: &data.BucketInfo{ + CID: cnrID, + Name: bktName, + ObjectLockEnabled: true, + }, + Settings: &data.BucketSettings{ + VersioningEnabled: true, + LockConfiguration: conf, + }, + } + + err = h.Layer().PutBucketSettings(ctx, sp) + require.NoError(t, err) +} + +func createTestObject(ctx context.Context, t *testing.T, h *handlerContext, bktName, objName string) { + content := make([]byte, 1024) + _, err := rand.Read(content) + require.NoError(t, err) + + _, err = h.Layer().PutObject(ctx, &layer.PutObjectParams{ + Bucket: bktName, + Object: objName, + Size: int64(len(content)), + Reader: bytes.NewReader(content), + Header: make(map[string]string), + }) + require.NoError(t, err) +} + +func prepareTestRequest(t *testing.T, bktName, objName string, body interface{}) (*httptest.ResponseRecorder, *http.Request) { + rawBody, err := xml.Marshal(body) + require.NoError(t, err) + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPut, defaultUrl, bytes.NewReader(rawBody)) + + reqInfo := api.NewReqInfo(w, r, api.ObjectRequest{Bucket: bktName, Object: objName}) + r = r.WithContext(api.SetReqInfo(r.Context(), reqInfo)) + + return w, r +} diff --git a/api/handler/locking.go b/api/handler/locking.go index 3c903aa5..1580cc55 100644 --- a/api/handler/locking.go +++ b/api/handler/locking.go @@ -86,18 +86,18 @@ func (h *handler) GetBucketObjectLockConfigHandler(w http.ResponseWriter, r *htt return } - settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo) - if err != nil { - h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err) - return - } - if !bktInfo.ObjectLockEnabled { h.logAndSendError(w, "object lock disabled", reqInfo, apiErrors.GetAPIError(apiErrors.ErrObjectLockConfigurationNotFound)) return } + settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo) + if err != nil { + h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err) + return + } + if settings.LockConfiguration == nil { settings.LockConfiguration = &data.ObjectLockConfiguration{} } @@ -171,9 +171,10 @@ func (h *handler) PutObjectLegalHoldHandler(w http.ResponseWriter, r *http.Reque } } else { ps := &layer.PutSystemObjectParams{ - BktInfo: bktInfo, - ObjName: objInfo.LegalHoldObject(), - Lock: &data.ObjectLock{LegalHold: true}, + BktInfo: bktInfo, + ObjName: objInfo.LegalHoldObject(), + Lock: &data.ObjectLock{LegalHold: true}, + Metadata: make(map[string]string), } if _, err = h.obj.PutSystemObject(r.Context(), ps); err != nil { h.logAndSendError(w, "couldn't put legal hold", reqInfo, err) @@ -278,15 +279,16 @@ func (h *handler) PutObjectRetentionHandler(w http.ResponseWriter, r *http.Reque return } - if lockInfo != nil && lockInfo.Headers[layer.AttributeComplianceMode] != "" { - h.logAndSendError(w, "couldn't change compliance lock mode", reqInfo, err) + if err = checkLockInfo(lockInfo, r.Header); err != nil { + h.logAndSendError(w, "couldn't change lock mode", reqInfo, err) return } ps := &layer.PutSystemObjectParams{ - BktInfo: bktInfo, - ObjName: objInfo.RetentionObject(), - Lock: lock, + BktInfo: bktInfo, + ObjName: objInfo.RetentionObject(), + Lock: lock, + Metadata: make(map[string]string), } if _, err = h.obj.PutSystemObject(r.Context(), ps); err != nil { h.logAndSendError(w, "couldn't put legal hold", reqInfo, err) @@ -294,6 +296,22 @@ func (h *handler) PutObjectRetentionHandler(w http.ResponseWriter, r *http.Reque } } +func checkLockInfo(lock *data.ObjectInfo, header http.Header) error { + if lock == nil { + return nil + } + + if lock.Headers[layer.AttributeComplianceMode] != "" { + return fmt.Errorf("it's forbidden to change compliance lock mode") + } + + if bypass, err := strconv.ParseBool(header.Get(api.AmzBypassGovernanceRetention)); err != nil || !bypass { + return fmt.Errorf("cannot bypass governance mode") + } + + return nil +} + func (h *handler) GetObjectRetentionHandler(w http.ResponseWriter, r *http.Request) { reqInfo := api.GetReqInfo(r.Context()) @@ -369,14 +387,16 @@ func checkLockConfiguration(conf *data.ObjectLockConfiguration) error { return nil } -func formObjectLock(objectLock *data.ObjectLock, bktInfo *data.BucketInfo, defaultConfig *data.ObjectLockConfiguration, header http.Header) error { +func formObjectLock(bktInfo *data.BucketInfo, defaultConfig *data.ObjectLockConfiguration, header http.Header) (*data.ObjectLock, error) { if !bktInfo.ObjectLockEnabled { if existLockHeaders(header) { - return apiErrors.GetAPIError(apiErrors.ErrObjectLockConfigurationNotFound) + return nil, apiErrors.GetAPIError(apiErrors.ErrObjectLockConfigurationNotFound) } - return nil + return nil, nil } + objectLock := &data.ObjectLock{} + if defaultConfig == nil { defaultConfig = &data.ObjectLockConfiguration{} } @@ -403,12 +423,12 @@ func formObjectLock(objectLock *data.ObjectLock, bktInfo *data.BucketInfo, defau if until != "" { retentionDate, err := time.Parse(time.RFC3339, until) if err != nil { - return fmt.Errorf("invalid header %s: '%s'", api.AmzObjectLockRetainUntilDate, until) + return nil, fmt.Errorf("invalid header %s: '%s'", api.AmzObjectLockRetainUntilDate, until) } objectLock.Until = retentionDate } - return nil + return objectLock, nil } func existLockHeaders(header http.Header) bool { @@ -418,15 +438,6 @@ func existLockHeaders(header http.Header) bool { } func formObjectLockFromRetention(retention *data.Retention, header http.Header) (*data.ObjectLock, error) { - var err error - var bypassGovernance bool - bypass := header.Get(api.AmzBypassGovernanceRetention) - if bypass != "" { - if bypassGovernance, err = strconv.ParseBool(bypass); err != nil { - return nil, fmt.Errorf("couldn't parse '%s' header", api.AmzBypassGovernanceRetention) - } - } - if retention.Mode != governanceMode && retention.Mode != complianceMode { return nil, fmt.Errorf("invalid retention mode: %s", retention.Mode) } @@ -441,9 +452,5 @@ func formObjectLockFromRetention(retention *data.Retention, header http.Header) IsCompliance: retention.Mode == complianceMode, } - if !lock.IsCompliance && !bypassGovernance { - return nil, fmt.Errorf("you cannot bypase governance mode") - } - return lock, nil } diff --git a/api/handler/locking_test.go b/api/handler/locking_test.go new file mode 100644 index 00000000..754c0dbd --- /dev/null +++ b/api/handler/locking_test.go @@ -0,0 +1,632 @@ +package handler + +import ( + "bytes" + "context" + "encoding/xml" + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/nspcc-dev/neofs-s3-gw/api" + "github.com/nspcc-dev/neofs-s3-gw/api/data" + apiErrors "github.com/nspcc-dev/neofs-s3-gw/api/errors" + "github.com/nspcc-dev/neofs-s3-gw/api/layer" + "github.com/stretchr/testify/require" +) + +const defaultUrl = "http://localhost/" + +func TestFormObjectLock(t *testing.T) { + for _, tc := range []struct { + name string + bktInfo *data.BucketInfo + config *data.ObjectLockConfiguration + header http.Header + expectedError bool + expectedLock *data.ObjectLock + }{ + { + name: "default days", + bktInfo: &data.BucketInfo{ObjectLockEnabled: true}, + config: &data.ObjectLockConfiguration{Rule: &data.ObjectLockRule{ + DefaultRetention: &data.DefaultRetention{Mode: complianceMode, Days: 1}}}, + expectedLock: &data.ObjectLock{IsCompliance: true, Until: time.Now().Add(24 * time.Hour)}, + }, + { + name: "default years", + bktInfo: &data.BucketInfo{ObjectLockEnabled: true}, + config: &data.ObjectLockConfiguration{Rule: &data.ObjectLockRule{ + DefaultRetention: &data.DefaultRetention{Mode: governanceMode, Years: 1}}}, + expectedLock: &data.ObjectLock{Until: time.Now().Add(365 * 24 * time.Hour)}, + }, + { + name: "basic override", + bktInfo: &data.BucketInfo{ObjectLockEnabled: true}, + config: &data.ObjectLockConfiguration{Rule: &data.ObjectLockRule{DefaultRetention: &data.DefaultRetention{Mode: complianceMode, Days: 1}}}, + header: map[string][]string{ + api.AmzObjectLockRetainUntilDate: {time.Now().Format(time.RFC3339)}, + api.AmzObjectLockMode: {governanceMode}, + api.AmzObjectLockLegalHold: {legalHoldOn}, + }, + expectedLock: &data.ObjectLock{Until: time.Now(), LegalHold: true}, + }, + { + name: "lock disabled error", + bktInfo: &data.BucketInfo{}, + header: map[string][]string{api.AmzObjectLockLegalHold: {legalHoldOn}}, + expectedError: true, + }, + { + name: "invalid time format error", + bktInfo: &data.BucketInfo{ObjectLockEnabled: true}, + header: map[string][]string{ + api.AmzObjectLockRetainUntilDate: {time.Now().Format(time.Layout)}, + }, + expectedError: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + actualObjLock, err := formObjectLock(tc.bktInfo, tc.config, tc.header) + if tc.expectedError { + require.Error(t, err) + return + } + + require.NoError(t, err) + assertObjectLocks(t, tc.expectedLock, actualObjLock) + }) + } +} + +func TestFormObjectLockFromRetention(t *testing.T) { + for _, tc := range []struct { + name string + retention *data.Retention + header http.Header + expectedError bool + expectedLock *data.ObjectLock + }{ + { + name: "basic compliance", + retention: &data.Retention{ + Mode: complianceMode, + RetainUntilDate: time.Now().Format(time.RFC3339), + }, + expectedLock: &data.ObjectLock{Until: time.Now(), IsCompliance: true}, + }, + { + name: "basic governance", + retention: &data.Retention{ + Mode: governanceMode, + RetainUntilDate: time.Now().Format(time.RFC3339), + }, + header: map[string][]string{ + api.AmzBypassGovernanceRetention: {strconv.FormatBool(true)}, + }, + expectedLock: &data.ObjectLock{Until: time.Now()}, + }, + { + name: "error invalid mode", + retention: &data.Retention{ + Mode: "", + RetainUntilDate: time.Now().Format(time.RFC3339), + }, + expectedError: true, + }, + { + name: "error invalid date", + retention: &data.Retention{ + Mode: governanceMode, + RetainUntilDate: "", + }, + expectedError: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + actualObjLock, err := formObjectLockFromRetention(tc.retention, tc.header) + if tc.expectedError { + require.Error(t, err) + return + } + + require.NoError(t, err) + assertObjectLocks(t, tc.expectedLock, actualObjLock) + }) + } +} + +func assertObjectLocks(t *testing.T, expected, actual *data.ObjectLock) { + require.Equal(t, expected.LegalHold, actual.LegalHold) + require.Equal(t, expected.IsCompliance, actual.IsCompliance) + require.InDelta(t, expected.Until.Unix(), actual.Until.Unix(), 1) +} + +func TestCheckLockObject(t *testing.T) { + for _, tc := range []struct { + name string + isCompliance bool + header http.Header + expectedError bool + }{ + { + name: "error governance bypass", + header: map[string][]string{ + api.AmzBypassGovernanceRetention: {strconv.FormatBool(false)}, + }, + expectedError: true, + }, + { + name: "error invalid governance bypass", + header: map[string][]string{ + api.AmzBypassGovernanceRetention: {"t r u e"}, + }, + expectedError: true, + }, + { + name: "error failed change compliance mode", + isCompliance: true, + expectedError: true, + }, + { + name: "valid", + header: map[string][]string{ + api.AmzBypassGovernanceRetention: {strconv.FormatBool(true)}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + header := make(map[string]string) + if tc.isCompliance { + header[layer.AttributeComplianceMode] = strconv.FormatBool(true) + } + + lockInfo := &data.ObjectInfo{Headers: header} + err := checkLockInfo(lockInfo, tc.header) + if tc.expectedError { + require.Error(t, err) + return + } + + require.NoError(t, err) + }) + } +} + +func TestLockConfiguration(t *testing.T) { + for _, tc := range []struct { + name string + configuration *data.ObjectLockConfiguration + expectedError bool + }{ + { + name: "basic empty", + configuration: &data.ObjectLockConfiguration{}, + }, + { + name: "basic compliance", + configuration: &data.ObjectLockConfiguration{ + ObjectLockEnabled: enabledValue, + Rule: &data.ObjectLockRule{ + DefaultRetention: &data.DefaultRetention{ + Days: 1, + Mode: complianceMode, + }, + }, + }, + }, + { + name: "basic governance", + configuration: &data.ObjectLockConfiguration{ + Rule: &data.ObjectLockRule{ + DefaultRetention: &data.DefaultRetention{ + Mode: governanceMode, + Years: 1, + }, + }, + }, + }, + { + name: "error invalid enabled", + configuration: &data.ObjectLockConfiguration{ + ObjectLockEnabled: "false", + }, + expectedError: true, + }, + { + name: "error invalid mode", + configuration: &data.ObjectLockConfiguration{ + Rule: &data.ObjectLockRule{ + DefaultRetention: &data.DefaultRetention{ + Mode: "", + }, + }, + }, + expectedError: true, + }, + { + name: "error no duration", + configuration: &data.ObjectLockConfiguration{ + Rule: &data.ObjectLockRule{ + DefaultRetention: &data.DefaultRetention{ + Mode: governanceMode, + }, + }, + }, + expectedError: true, + }, + { + name: "error both durations", + configuration: &data.ObjectLockConfiguration{ + Rule: &data.ObjectLockRule{ + DefaultRetention: &data.DefaultRetention{ + Days: 1, + Mode: governanceMode, + Years: 1, + }, + }, + }, + expectedError: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + err := checkLockConfiguration(tc.configuration) + if tc.expectedError { + require.Error(t, err) + return + } + + require.NoError(t, err) + }) + } +} + +func TestPutBucketLockConfigurationHandler(t *testing.T) { + ctx := context.Background() + hc := prepareHandlerContext(t) + + bktLockDisabled := "bucket-lock-disabled" + createTestBucket(ctx, t, hc, bktLockDisabled) + + bktLockEnabled := "bucket-lock-enabled" + createTestBucketWithLock(ctx, t, hc, bktLockEnabled, nil) + + bktLockEnabledWithOldConfig := "bucket-lock-enabled-old-conf" + createTestBucketWithLock(ctx, t, hc, bktLockEnabledWithOldConfig, + &data.ObjectLockConfiguration{ + Rule: &data.ObjectLockRule{ + DefaultRetention: &data.DefaultRetention{ + Days: 1, + Mode: complianceMode, + }, + }, + }) + + for _, tc := range []struct { + name string + bucket string + expectedError apiErrors.Error + noError bool + configuration *data.ObjectLockConfiguration + }{ + { + name: "bkt not found", + expectedError: apiErrors.GetAPIError(apiErrors.ErrNoSuchBucket), + }, + { + name: "bkt lock disabled", + bucket: bktLockDisabled, + expectedError: apiErrors.GetAPIError(apiErrors.ErrObjectLockConfigurationNotAllowed), + }, + { + name: "invalid configuration", + bucket: bktLockEnabled, + expectedError: apiErrors.GetAPIError(apiErrors.ErrInternalError), + configuration: &data.ObjectLockConfiguration{ObjectLockEnabled: "dummy"}, + }, + { + name: "basic", + bucket: bktLockEnabled, + noError: true, + configuration: &data.ObjectLockConfiguration{}, + }, + { + name: "basic override", + bucket: bktLockEnabledWithOldConfig, + noError: true, + configuration: &data.ObjectLockConfiguration{ + Rule: &data.ObjectLockRule{ + DefaultRetention: &data.DefaultRetention{ + Mode: governanceMode, + Years: 1, + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + body, err := xml.Marshal(tc.configuration) + require.NoError(t, err) + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPut, defaultUrl, bytes.NewReader(body)) + r = r.WithContext(api.SetReqInfo(r.Context(), api.NewReqInfo(w, r, api.ObjectRequest{Bucket: tc.bucket}))) + + hc.Handler().PutBucketObjectLockConfigHandler(w, r) + + if !tc.noError { + assertS3Error(t, w, tc.expectedError) + return + } + + bktInfo, err := hc.Layer().GetBucketInfo(ctx, tc.bucket) + require.NoError(t, err) + bktSettings, err := hc.Layer().GetBucketSettings(ctx, bktInfo) + require.NoError(t, err) + actualConf := bktSettings.LockConfiguration + require.True(t, bktSettings.VersioningEnabled) + require.Equal(t, tc.configuration.ObjectLockEnabled, actualConf.ObjectLockEnabled) + require.Equal(t, tc.configuration.Rule, actualConf.Rule) + }) + } +} + +func TestGetBucketLockConfigurationHandler(t *testing.T) { + ctx := context.Background() + hc := prepareHandlerContext(t) + + bktLockDisabled := "bucket-lock-disabled" + createTestBucket(ctx, t, hc, bktLockDisabled) + + bktLockEnabled := "bucket-lock-enabled" + createTestBucketWithLock(ctx, t, hc, bktLockEnabled, nil) + + oldConfig := &data.ObjectLockConfiguration{ + Rule: &data.ObjectLockRule{ + DefaultRetention: &data.DefaultRetention{ + Days: 1, + Mode: complianceMode, + }, + }, + } + bktLockEnabledWithOldConfig := "bucket-lock-enabled-old-conf" + createTestBucketWithLock(ctx, t, hc, bktLockEnabledWithOldConfig, oldConfig) + + for _, tc := range []struct { + name string + bucket string + expectedError apiErrors.Error + noError bool + expectedConf *data.ObjectLockConfiguration + }{ + { + name: "bkt not found", + expectedError: apiErrors.GetAPIError(apiErrors.ErrNoSuchBucket), + }, + { + name: "bkt lock disabled", + bucket: bktLockDisabled, + expectedError: apiErrors.GetAPIError(apiErrors.ErrObjectLockConfigurationNotFound), + }, + { + name: "bkt lock enabled empty default", + bucket: bktLockEnabled, + noError: true, + expectedConf: &data.ObjectLockConfiguration{ObjectLockEnabled: enabledValue}, + }, + { + name: "bkt lock enabled", + bucket: bktLockEnabledWithOldConfig, + noError: true, + expectedConf: oldConfig, + }, + } { + t.Run(tc.name, func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPut, defaultUrl, bytes.NewReader(nil)) + r = r.WithContext(api.SetReqInfo(r.Context(), api.NewReqInfo(w, r, api.ObjectRequest{Bucket: tc.bucket}))) + + hc.Handler().GetBucketObjectLockConfigHandler(w, r) + + if !tc.noError { + assertS3Error(t, w, tc.expectedError) + return + } + + actualConf := &data.ObjectLockConfiguration{} + err := xml.NewDecoder(w.Result().Body).Decode(actualConf) + require.NoError(t, err) + + require.Equal(t, tc.expectedConf.ObjectLockEnabled, actualConf.ObjectLockEnabled) + require.Equal(t, tc.expectedConf.Rule, actualConf.Rule) + }) + } +} + +func assertS3Error(t *testing.T, w *httptest.ResponseRecorder, expectedError apiErrors.Error) { + actualErrorResponse := &api.ErrorResponse{} + err := xml.NewDecoder(w.Result().Body).Decode(actualErrorResponse) + require.NoError(t, err) + + require.Equal(t, expectedError.HTTPStatusCode, w.Code) + require.Equal(t, expectedError.Code, actualErrorResponse.Code) + + if expectedError.ErrCode != apiErrors.ErrInternalError { + require.Equal(t, expectedError.Description, actualErrorResponse.Message) + } +} + +func TestObjectLegalHold(t *testing.T) { + ctx := context.Background() + hc := prepareHandlerContext(t) + + bktName := "bucket-lock-enabled" + createTestBucketWithLock(ctx, t, hc, bktName, nil) + + objName := "obj-for-legal-hold" + createTestObject(ctx, t, hc, bktName, objName) + + w, r := prepareTestRequest(t, bktName, objName, nil) + hc.Handler().GetObjectLegalHoldHandler(w, r) + assertLegalHold(t, w, legalHoldOff) + + w, r = prepareTestRequest(t, bktName, objName, &data.LegalHold{Status: legalHoldOn}) + hc.Handler().PutObjectLegalHoldHandler(w, r) + require.Equal(t, http.StatusOK, w.Code) + + w, r = prepareTestRequest(t, bktName, objName, nil) + hc.Handler().GetObjectLegalHoldHandler(w, r) + assertLegalHold(t, w, legalHoldOn) + + // to make sure put hold is idempotent operation + w, r = prepareTestRequest(t, bktName, objName, &data.LegalHold{Status: legalHoldOn}) + hc.Handler().PutObjectLegalHoldHandler(w, r) + require.Equal(t, http.StatusOK, w.Code) + + w, r = prepareTestRequest(t, bktName, objName, &data.LegalHold{Status: legalHoldOff}) + hc.Handler().PutObjectLegalHoldHandler(w, r) + require.Equal(t, http.StatusOK, w.Code) + + w, r = prepareTestRequest(t, bktName, objName, nil) + hc.Handler().GetObjectLegalHoldHandler(w, r) + assertLegalHold(t, w, legalHoldOff) + + // to make sure put hold is idempotent operation + w, r = prepareTestRequest(t, bktName, objName, &data.LegalHold{Status: legalHoldOff}) + hc.Handler().PutObjectLegalHoldHandler(w, r) + require.Equal(t, http.StatusOK, w.Code) +} + +func assertLegalHold(t *testing.T, w *httptest.ResponseRecorder, status string) { + actualHold := &data.LegalHold{} + err := xml.NewDecoder(w.Result().Body).Decode(actualHold) + require.NoError(t, err) + require.Equal(t, status, actualHold.Status) + require.Equal(t, http.StatusOK, w.Code) +} + +func TestObjectRetention(t *testing.T) { + ctx := context.Background() + hc := prepareHandlerContext(t) + + bktName := "bucket-lock-enabled" + createTestBucketWithLock(ctx, t, hc, bktName, nil) + + objName := "obj-for-retention" + createTestObject(ctx, t, hc, bktName, objName) + + w, r := prepareTestRequest(t, bktName, objName, nil) + hc.Handler().GetObjectRetentionHandler(w, r) + assertS3Error(t, w, apiErrors.GetAPIError(apiErrors.ErrNoSuchKey)) + + retention := &data.Retention{Mode: governanceMode, RetainUntilDate: time.Now().Format(time.RFC3339)} + w, r = prepareTestRequest(t, bktName, objName, retention) + hc.Handler().PutObjectRetentionHandler(w, r) + require.Equal(t, http.StatusOK, w.Code) + + w, r = prepareTestRequest(t, bktName, objName, nil) + hc.Handler().GetObjectRetentionHandler(w, r) + assertRetention(t, w, retention) + + retention = &data.Retention{Mode: governanceMode, RetainUntilDate: time.Now().Format(time.RFC3339)} + w, r = prepareTestRequest(t, bktName, objName, retention) + hc.Handler().PutObjectRetentionHandler(w, r) + assertS3Error(t, w, apiErrors.GetAPIError(apiErrors.ErrInternalError)) + + retention = &data.Retention{Mode: complianceMode, RetainUntilDate: time.Now().Format(time.RFC3339)} + w, r = prepareTestRequest(t, bktName, objName, retention) + r.Header.Set(api.AmzBypassGovernanceRetention, strconv.FormatBool(true)) + hc.Handler().PutObjectRetentionHandler(w, r) + require.Equal(t, http.StatusOK, w.Code) + + w, r = prepareTestRequest(t, bktName, objName, nil) + hc.Handler().GetObjectRetentionHandler(w, r) + assertRetention(t, w, retention) + + w, r = prepareTestRequest(t, bktName, objName, retention) + r.Header.Set(api.AmzBypassGovernanceRetention, strconv.FormatBool(true)) + hc.Handler().PutObjectRetentionHandler(w, r) + assertS3Error(t, w, apiErrors.GetAPIError(apiErrors.ErrInternalError)) +} + +func assertRetention(t *testing.T, w *httptest.ResponseRecorder, retention *data.Retention) { + actualRetention := &data.Retention{} + err := xml.NewDecoder(w.Result().Body).Decode(actualRetention) + require.NoError(t, err) + require.Equal(t, retention.Mode, actualRetention.Mode) + require.Equal(t, retention.RetainUntilDate, actualRetention.RetainUntilDate) + require.Equal(t, http.StatusOK, w.Code) +} + +func TestPutObjectWithLock(t *testing.T) { + ctx := context.Background() + hc := prepareHandlerContext(t) + + bktName := "bucket-lock-enabled" + lockConfig := &data.ObjectLockConfiguration{ + ObjectLockEnabled: enabledValue, + Rule: &data.ObjectLockRule{ + DefaultRetention: &data.DefaultRetention{ + Days: 1, + Mode: governanceMode, + }, + }, + } + createTestBucketWithLock(ctx, t, hc, bktName, lockConfig) + + objDefault := "obj-default-retention" + + w, r := prepareTestRequest(t, bktName, objDefault, nil) + hc.Handler().PutObjectHandler(w, r) + require.Equal(t, http.StatusOK, w.Code) + + w, r = prepareTestRequest(t, bktName, objDefault, nil) + hc.Handler().GetObjectRetentionHandler(w, r) + expectedRetention := &data.Retention{ + Mode: governanceMode, + RetainUntilDate: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + } + assertRetentionApproximate(t, w, expectedRetention, 1) + + w, r = prepareTestRequest(t, bktName, objDefault, nil) + hc.Handler().GetObjectLegalHoldHandler(w, r) + assertLegalHold(t, w, legalHoldOff) + + objOverride := "obj-override-retention" + w, r = prepareTestRequest(t, bktName, objOverride, nil) + r.Header.Set(api.AmzObjectLockMode, complianceMode) + r.Header.Set(api.AmzObjectLockLegalHold, legalHoldOn) + r.Header.Set(api.AmzObjectLockRetainUntilDate, time.Now().Add(2*24*time.Hour).Format(time.RFC3339)) + hc.Handler().PutObjectHandler(w, r) + require.Equal(t, http.StatusOK, w.Code) + + w, r = prepareTestRequest(t, bktName, objOverride, nil) + hc.Handler().GetObjectRetentionHandler(w, r) + expectedRetention = &data.Retention{ + Mode: complianceMode, + RetainUntilDate: time.Now().Add(2 * 24 * time.Hour).Format(time.RFC3339), + } + assertRetentionApproximate(t, w, expectedRetention, 1) + + w, r = prepareTestRequest(t, bktName, objOverride, nil) + hc.Handler().GetObjectLegalHoldHandler(w, r) + assertLegalHold(t, w, legalHoldOn) +} + +func assertRetentionApproximate(t *testing.T, w *httptest.ResponseRecorder, retention *data.Retention, delta float64) { + actualRetention := &data.Retention{} + err := xml.NewDecoder(w.Result().Body).Decode(actualRetention) + require.NoError(t, err) + require.Equal(t, retention.Mode, actualRetention.Mode) + require.Equal(t, http.StatusOK, w.Code) + + actualUntil, err := time.Parse(time.RFC3339, actualRetention.RetainUntilDate) + require.NoError(t, err) + + expectedUntil, err := time.Parse(time.RFC3339, retention.RetainUntilDate) + require.NoError(t, err) + + require.InDelta(t, expectedUntil.Unix(), actualUntil.Unix(), delta) +} diff --git a/api/handler/put.go b/api/handler/put.go index 82f0e1f8..6155b229 100644 --- a/api/handler/put.go +++ b/api/handler/put.go @@ -215,7 +215,8 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) { return } - if err = formObjectLock(params.Lock, bktInfo, settings.LockConfiguration, r.Header); err != nil { + params.Lock, err = formObjectLock(bktInfo, settings.LockConfiguration, r.Header) + if err != nil { h.logAndSendError(w, "could not form object lock", reqInfo, err) return } diff --git a/api/layer/container.go b/api/layer/container.go index 0bb63747..d675ab14 100644 --- a/api/layer/container.go +++ b/api/layer/container.go @@ -29,7 +29,7 @@ type ( const ( attributeLocationConstraint = ".s3-location-constraint" - attributeLockEnabled = "LockEnabled" + AttributeLockEnabled = "LockEnabled" ) func (n *layer) containerInfo(ctx context.Context, idCnr *cid.ID) (*data.BucketInfo, error) { @@ -73,7 +73,7 @@ func (n *layer) containerInfo(ctx context.Context, idCnr *cid.ID) (*data.BucketI info.Created = time.Unix(unix, 0) case attributeLocationConstraint: info.LocationConstraint = val - case attributeLockEnabled: + case AttributeLockEnabled: info.ObjectLockEnabled, err = strconv.ParseBool(val) if err != nil { log.Error("could not parse container object lock enabled attribute", diff --git a/api/layer/object.go b/api/layer/object.go index 873a6104..62dfcd69 100644 --- a/api/layer/object.go +++ b/api/layer/object.go @@ -3,6 +3,7 @@ package layer import ( "context" "errors" + "fmt" "io" "sort" "strings" @@ -201,8 +202,17 @@ func (n *layer) objectPut(ctx context.Context, bkt *data.BucketInfo, p *PutObjec } if p.Lock != nil { - // todo form lock system object - // attributes = append(attributes, attributesFromLock(p.Lock)...) + objInfo := &data.ObjectInfo{ID: oid, Name: p.Object} + if p.Lock.LegalHold { + if err = n.putLockObject(ctx, bkt, objInfo.LegalHoldObject(), p.Lock); err != nil { + return nil, err + } + } + if !p.Lock.Until.IsZero() { + if err = n.putLockObject(ctx, bkt, objInfo.RetentionObject(), p.Lock); err != nil { + return nil, err + } + } } meta, err := n.objectHead(ctx, bkt.CID, id) @@ -249,6 +259,21 @@ func (n *layer) objectPut(ctx context.Context, bkt *data.BucketInfo, p *PutObjec }, nil } +func (n *layer) putLockObject(ctx context.Context, bktInfo *data.BucketInfo, objName string, lock *data.ObjectLock) error { + ps := &PutSystemObjectParams{ + BktInfo: bktInfo, + ObjName: objName, + Lock: lock, + Metadata: make(map[string]string), + } + + if _, err := n.PutSystemObject(ctx, ps); err != nil { + return fmt.Errorf("coudln't add lock for '%s': %w", objName, err) + } + + return nil +} + func updateCRDT2PSetHeaders(header map[string]string, versions *objectVersions, versioningEnabled bool) []*oid.ID { if !versioningEnabled { header[versionsUnversionedAttr] = "true" @@ -638,7 +663,7 @@ func (n *layer) listAllObjects(ctx context.Context, p ListObjectsParamsCommon) ( } func (n *layer) isVersioningEnabled(ctx context.Context, bktInfo *data.BucketInfo) bool { - settings, err := n.getBucketSettings(ctx, bktInfo) + settings, err := n.GetBucketSettings(ctx, bktInfo) if err != nil { n.log.Warn("couldn't get versioning settings object", zap.Error(err)) return false diff --git a/api/layer/system_object.go b/api/layer/system_object.go index 01342ed1..c085ee3d 100644 --- a/api/layer/system_object.go +++ b/api/layer/system_object.go @@ -221,7 +221,8 @@ func systemObjectKey(bktInfo *data.BucketInfo, obj string) string { } func (n *layer) GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error) { - if settings := n.systemCache.GetSettings(bktInfo.SettingsObjectName()); settings != nil { + systemKey := systemObjectKey(bktInfo, bktInfo.SettingsObjectName()) + if settings := n.systemCache.GetSettings(systemKey); settings != nil { return settings, nil } @@ -235,7 +236,7 @@ func (n *layer) GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo) return nil, err } - if err = n.systemCache.PutSettings(bktInfo.SettingsObjectName(), settings); err != nil { + if err = n.systemCache.PutSettings(systemKey, settings); err != nil { n.log.Warn("couldn't put system meta to objects cache", zap.Stringer("object id", obj.ID()), zap.Stringer("bucket id", bktInfo.CID), @@ -267,7 +268,8 @@ func (n *layer) PutBucketSettings(ctx context.Context, p *PutSettingsParams) err return errors.GetAPIError(errors.ErrInternalError) } - if err = n.systemCache.PutSettings(p.BktInfo.SettingsObjectName(), p.Settings); err != nil { + systemKey := systemObjectKey(p.BktInfo, p.BktInfo.SettingsObjectName()) + if err = n.systemCache.PutSettings(systemKey, p.Settings); err != nil { n.log.Error("couldn't cache system object", zap.Error(err)) } @@ -276,7 +278,7 @@ func (n *layer) PutBucketSettings(ctx context.Context, p *PutSettingsParams) err func attributesFromLock(lock *data.ObjectLock) []*object.Attribute { var result []*object.Attribute - if !lock.LegalHold { + if !lock.Until.IsZero() { attrRetainUntil := object.NewAttribute() attrRetainUntil.SetKey(AttributeRetainUntil) attrRetainUntil.SetValue(lock.Until.Format(time.RFC3339)) diff --git a/api/layer/versioning.go b/api/layer/versioning.go index f5cf77d7..cbffd4e4 100644 --- a/api/layer/versioning.go +++ b/api/layer/versioning.go @@ -300,7 +300,7 @@ func (n *layer) PutBucketVersioning(ctx context.Context, p *PutSettingsParams) ( Reader: nil, } - return n.putSystemObject(ctx, s) + return n.PutSystemObject(ctx, s) } func (n *layer) GetBucketVersioning(ctx context.Context, bucketName string) (*data.BucketSettings, error) { @@ -309,7 +309,7 @@ func (n *layer) GetBucketVersioning(ctx context.Context, bucketName string) (*da return nil, err } - return n.getBucketSettings(ctx, bktInfo) + return n.GetBucketSettings(ctx, bktInfo) } func (n *layer) ListObjectVersions(ctx context.Context, p *ListObjectVersionsParams) (*ListObjectVersionsInfo, error) { @@ -398,15 +398,6 @@ func contains(list []string, elem string) bool { return false } -func (n *layer) getBucketSettings(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error) { - objInfo, err := n.HeadSystemObject(ctx, bktInfo, bktInfo.SettingsObjectName()) - if err != nil { - return nil, err - } - - return objectInfoToBucketSettings(objInfo), nil -} - func objectInfoToBucketSettings(info *data.ObjectInfo) *data.BucketSettings { res := &data.BucketSettings{} diff --git a/api/layer/versioning_test.go b/api/layer/versioning_test.go index 8fffac77..331fc92a 100644 --- a/api/layer/versioning_test.go +++ b/api/layer/versioning_test.go @@ -3,10 +3,6 @@ package layer import ( "bytes" "context" - "crypto/rand" - "crypto/sha256" - "fmt" - "io" "strconv" "strings" "testing" @@ -14,207 +10,18 @@ import ( "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neofs-s3-gw/api" "github.com/nspcc-dev/neofs-s3-gw/api/data" + "github.com/nspcc-dev/neofs-s3-gw/api/mock" "github.com/nspcc-dev/neofs-s3-gw/creds/accessbox" - "github.com/nspcc-dev/neofs-sdk-go/container" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" "github.com/nspcc-dev/neofs-sdk-go/logger" "github.com/nspcc-dev/neofs-sdk-go/object" "github.com/nspcc-dev/neofs-sdk-go/object/address" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" "github.com/nspcc-dev/neofs-sdk-go/object/id/test" - "github.com/nspcc-dev/neofs-sdk-go/owner" tokentest "github.com/nspcc-dev/neofs-sdk-go/token/test" "github.com/stretchr/testify/require" ) -type testNeoFS struct { - NeoFS - - objects map[string]*object.Object - containers map[string]*container.Container - currentEpoch uint64 -} - -func (t *testNeoFS) CreateContainer(_ context.Context, prm PrmContainerCreate) (*cid.ID, error) { - var opts []container.Option - - opts = append(opts, - container.WithOwnerID(&prm.Creator), - container.WithPolicy(&prm.Policy), - container.WithCustomBasicACL(prm.BasicACL), - container.WithAttribute(container.AttributeTimestamp, strconv.FormatInt(prm.Time.Unix(), 10)), - ) - - if prm.Name != "" { - opts = append(opts, container.WithAttribute(container.AttributeName, prm.Name)) - } - - cnr := container.New(opts...) - cnr.SetSessionToken(prm.SessionToken) - - if prm.Name != "" { - container.SetNativeName(cnr, prm.Name) - } - - b := make([]byte, 32) - if _, err := io.ReadFull(rand.Reader, b); err != nil { - return nil, err - } - - id := cid.New() - id.SetSHA256(sha256.Sum256(b)) - t.containers[id.String()] = cnr - - return id, nil -} - -func (t *testNeoFS) Container(_ context.Context, id cid.ID) (*container.Container, error) { - for k, v := range t.containers { - if k == id.String() { - return v, nil - } - } - - return nil, fmt.Errorf("container not found " + id.String()) -} - -func (t *testNeoFS) UserContainers(_ context.Context, _ owner.ID) ([]cid.ID, error) { - var res []cid.ID - for k := range t.containers { - var idCnr cid.ID - if err := idCnr.Parse(k); err != nil { - return nil, err - } - res = append(res, idCnr) - } - - return res, nil -} - -func (t *testNeoFS) SelectObjects(_ context.Context, prm PrmObjectSelect) ([]oid.ID, error) { - var filters object.SearchFilters - filters.AddRootFilter() - - if prm.FilePrefix != "" { - filters.AddFilter(object.AttributeFileName, prm.FilePrefix, object.MatchCommonPrefix) - } - - if prm.ExactAttribute[0] != "" { - filters.AddFilter(prm.ExactAttribute[0], prm.ExactAttribute[1], object.MatchStringEqual) - } - - cidStr := prm.Container.String() - - var res []oid.ID - - if len(filters) == 1 { - for k, v := range t.objects { - if strings.Contains(k, cidStr) { - res = append(res, *v.ID()) - } - } - return res, nil - } - - filter := filters[1] - if len(filters) != 2 || filter.Operation() != object.MatchStringEqual || - (filter.Header() != object.AttributeFileName && filter.Header() != objectSystemAttributeName) { - return nil, fmt.Errorf("usupported filters") - } - - for k, v := range t.objects { - if strings.Contains(k, cidStr) && isMatched(v.Attributes(), filter) { - res = append(res, *v.ID()) - } - } - - return res, nil -} - -func (t *testNeoFS) ReadObject(_ context.Context, prm PrmObjectRead) (*ObjectPart, error) { - var addr address.Address - addr.SetContainerID(&prm.Container) - addr.SetObjectID(&prm.Object) - - sAddr := addr.String() - - if obj, ok := t.objects[sAddr]; ok { - return &ObjectPart{ - Head: obj, - Payload: io.NopCloser(bytes.NewReader(obj.Payload())), - }, nil - } - - return nil, fmt.Errorf("object not found " + addr.String()) -} - -func (t *testNeoFS) CreateObject(_ context.Context, prm PrmObjectCreate) (*oid.ID, error) { - id := test.ID() - - attrs := make([]object.Attribute, 0) - - if prm.Filename != "" { - a := object.NewAttribute() - a.SetKey(object.AttributeFileName) - a.SetValue(prm.Filename) - attrs = append(attrs, *a) - } - - for i := range prm.Attributes { - a := object.NewAttribute() - a.SetKey(prm.Attributes[i][0]) - a.SetValue(prm.Attributes[i][1]) - attrs = append(attrs, *a) - } - - obj := object.New() - obj.SetContainerID(&prm.Container) - obj.SetID(id) - obj.SetPayloadSize(prm.PayloadSize) - obj.SetAttributes(attrs...) - obj.SetCreationEpoch(t.currentEpoch) - t.currentEpoch++ - - if prm.Payload != nil { - all, err := io.ReadAll(prm.Payload) - if err != nil { - return nil, err - } - obj.SetPayload(all) - } - - addr := newAddress(obj.ContainerID(), obj.ID()) - t.objects[addr.String()] = obj - return obj.ID(), nil -} - -func (t *testNeoFS) DeleteObject(_ context.Context, prm PrmObjectDelete) error { - var addr address.Address - addr.SetContainerID(&prm.Container) - addr.SetObjectID(&prm.Object) - - delete(t.objects, addr.String()) - - return nil -} - -func newTestPool() *testNeoFS { - return &testNeoFS{ - objects: make(map[string]*object.Object), - containers: make(map[string]*container.Container), - } -} - -func isMatched(attributes []object.Attribute, filter object.SearchFilter) bool { - for _, attr := range attributes { - if attr.Key() == filter.Header() && attr.Value() == filter.Value() { - return true - } - } - - return false -} - func (tc *testContext) putObject(content []byte) *data.ObjectInfo { objInfo, err := tc.layer.PutObject(tc.ctx, &PutObjectParams{ Bucket: tc.bktID.String(), @@ -307,7 +114,7 @@ func (tc *testContext) checkListObjects(ids ...*oid.ID) { } func (tc *testContext) getSystemObject(objectName string) *object.Object { - for _, obj := range tc.testNeoFS.objects { + for _, obj := range tc.testNeoFS.Objects { for _, attr := range obj.Attributes() { if attr.Key() == objectSystemAttributeName && attr.Value() == objectName { return obj @@ -325,7 +132,7 @@ type testContext struct { bktID *cid.ID bktInfo *data.BucketInfo obj string - testNeoFS *testNeoFS + testNeoFS *mock.TestNeoFS } func prepareContext(t *testing.T, cachesConfig ...*CachesConfig) *testContext { @@ -343,7 +150,7 @@ func prepareContext(t *testing.T, cachesConfig ...*CachesConfig) *testContext { }) l, err := logger.New(logger.WithTraceLevel("panic")) require.NoError(t, err) - tp := newTestPool() + tp := mock.NewTestPool() bktName := "testbucket1" bktID, err := tp.CreateContainer(ctx, PrmContainerCreate{ @@ -821,7 +628,7 @@ func TestSystemObjectsVersioning(t *testing.T) { addr.SetObjectID(objMeta.ID()) // simulate failed deletion - tc.testNeoFS.objects[addr.String()] = objMeta + tc.testNeoFS.Objects[addr.String()] = objMeta bktInfo := &data.BucketInfo{ Name: tc.bkt, @@ -853,7 +660,7 @@ func TestDeleteSystemObjectsVersioning(t *testing.T) { require.NoError(t, err) // simulate failed deletion - tc.testNeoFS.objects[newAddress(objMeta.ContainerID(), objMeta.ID()).String()] = objMeta + tc.testNeoFS.Objects[newAddress(objMeta.ContainerID(), objMeta.ID()).String()] = objMeta tagging, err := tc.layer.GetBucketTagging(tc.ctx, tc.bkt) require.NoError(t, err) diff --git a/api/mock/sdk_pool_mock.go b/api/mock/sdk_pool_mock.go new file mode 100644 index 00000000..55988035 --- /dev/null +++ b/api/mock/sdk_pool_mock.go @@ -0,0 +1,216 @@ +package mock + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "fmt" + "io" + "strings" + + "github.com/nspcc-dev/neofs-sdk-go/accounting" + "github.com/nspcc-dev/neofs-sdk-go/client" + "github.com/nspcc-dev/neofs-sdk-go/container" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/eacl" + "github.com/nspcc-dev/neofs-sdk-go/object" + "github.com/nspcc-dev/neofs-sdk-go/owner" + "github.com/nspcc-dev/neofs-sdk-go/pool" + "github.com/nspcc-dev/neofs-sdk-go/session" +) + +type TestPool struct { + Objects map[string]*object.Object + Containers map[string]*container.Container + CurrentEpoch uint64 +} + +func NewTestPool() *TestPool { + return &TestPool{ + Objects: make(map[string]*object.Object), + Containers: make(map[string]*container.Container), + } +} + +func (t *TestPool) PutObject(ctx context.Context, params *client.PutObjectParams, option ...pool.CallOption) (*object.ID, error) { + b := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, b); err != nil { + return nil, err + } + + oid := object.NewID() + oid.SetSHA256(sha256.Sum256(b)) + + raw := object.NewRawFrom(params.Object()) + raw.SetID(oid) + raw.SetCreationEpoch(t.CurrentEpoch) + t.CurrentEpoch++ + + if params.PayloadReader() != nil { + all, err := io.ReadAll(params.PayloadReader()) + if err != nil { + return nil, err + } + raw.SetPayload(all) + } + raw.SetPayloadSize(uint64(len(raw.Payload()))) + + addr := newAddress(raw.ContainerID(), raw.ID()) + t.Objects[addr.String()] = raw.Object() + return raw.ID(), nil +} + +func (t *TestPool) DeleteObject(ctx context.Context, params *client.DeleteObjectParams, option ...pool.CallOption) error { + delete(t.Objects, params.Address().String()) + return nil +} + +func (t *TestPool) GetObject(ctx context.Context, params *client.GetObjectParams, option ...pool.CallOption) (*object.Object, error) { + if obj, ok := t.Objects[params.Address().String()]; ok { + if params.PayloadWriter() != nil { + _, err := params.PayloadWriter().Write(obj.Payload()) + if err != nil { + return nil, err + } + } + return obj, nil + } + + return nil, fmt.Errorf("object not found " + params.Address().String()) +} + +func (t *TestPool) GetObjectHeader(ctx context.Context, params *client.ObjectHeaderParams, option ...pool.CallOption) (*object.Object, error) { + p := new(client.GetObjectParams).WithAddress(params.Address()) + return t.GetObject(ctx, p) +} + +func (t *TestPool) ObjectPayloadRangeData(ctx context.Context, params *client.RangeDataParams, option ...pool.CallOption) ([]byte, error) { + panic("implement me") +} + +func (t *TestPool) ObjectPayloadRangeSHA256(ctx context.Context, params *client.RangeChecksumParams, option ...pool.CallOption) ([][32]byte, error) { + panic("implement me") +} + +func (t *TestPool) ObjectPayloadRangeTZ(ctx context.Context, params *client.RangeChecksumParams, option ...pool.CallOption) ([][64]byte, error) { + panic("implement me") +} + +func (t *TestPool) SearchObject(ctx context.Context, params *client.SearchObjectParams, option ...pool.CallOption) ([]*object.ID, error) { + cidStr := params.ContainerID().String() + + var res []*object.ID + + if len(params.SearchFilters()) == 1 { + for k, v := range t.Objects { + if strings.Contains(k, cidStr) { + res = append(res, v.ID()) + } + } + return res, nil + } + + filter := params.SearchFilters()[1] + if len(params.SearchFilters()) != 2 || filter.Operation() != object.MatchStringEqual || + (filter.Header() != object.AttributeFileName && filter.Header() != "S3-System-name") { + return nil, fmt.Errorf("usupported filters") + } + + for k, v := range t.Objects { + if strings.Contains(k, cidStr) && isMatched(v.Attributes(), filter) { + res = append(res, v.ID()) + } + } + + return res, nil +} + +func isMatched(attributes []*object.Attribute, filter object.SearchFilter) bool { + for _, attr := range attributes { + if attr.Key() == filter.Header() && attr.Value() == filter.Value() { + return true + } + } + + return false +} + +func (t *TestPool) PutContainer(ctx context.Context, container *container.Container, option ...pool.CallOption) (*cid.ID, error) { + b := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, b); err != nil { + return nil, err + } + + id := cid.New() + id.SetSHA256(sha256.Sum256(b)) + t.Containers[id.String()] = container + + return id, nil +} + +func (t *TestPool) GetContainer(ctx context.Context, id *cid.ID, option ...pool.CallOption) (*container.Container, error) { + for k, v := range t.Containers { + if k == id.String() { + return v, nil + } + } + + return nil, fmt.Errorf("container not found " + id.String()) +} + +func (t *TestPool) ListContainers(ctx context.Context, id *owner.ID, option ...pool.CallOption) ([]*cid.ID, error) { + var res []*cid.ID + for k := range t.Containers { + cID := cid.New() + if err := cID.Parse(k); err != nil { + return nil, err + } + res = append(res, cID) + } + + return res, nil +} + +func (t *TestPool) DeleteContainer(ctx context.Context, id *cid.ID, option ...pool.CallOption) error { + delete(t.Containers, id.String()) + return nil +} + +func (t *TestPool) GetEACL(ctx context.Context, id *cid.ID, option ...pool.CallOption) (*eacl.Table, error) { + panic("implement me") +} + +func (t *TestPool) Balance(ctx context.Context, owner *owner.ID, opts ...pool.CallOption) (*accounting.Decimal, error) { + panic("implement me") +} + +func (t *TestPool) SetEACL(ctx context.Context, table *eacl.Table, option ...pool.CallOption) error { + panic("implement me") +} + +func (t *TestPool) AnnounceContainerUsedSpace(ctx context.Context, announcements []container.UsedSpaceAnnouncement, option ...pool.CallOption) error { + panic("implement me") +} + +func (t *TestPool) Connection() (pool.Client, *session.Token, error) { + panic("implement me") +} + +func (t *TestPool) Close() { + panic("implement me") +} + +func (t *TestPool) OwnerID() *owner.ID { + return nil +} + +func (t *TestPool) WaitForContainerPresence(ctx context.Context, id *cid.ID, params *pool.ContainerPollingParams) error { + return nil +} + +func newAddress(cid *cid.ID, oid *object.ID) *object.Address { + address := object.NewAddress() + address.SetContainerID(cid) + address.SetObjectID(oid) + return address +} diff --git a/api/resolver/resolver.go b/api/resolver/resolver.go index 02fb8e3f..a8f76fab 100644 --- a/api/resolver/resolver.go +++ b/api/resolver/resolver.go @@ -35,6 +35,10 @@ type BucketResolver struct { next *BucketResolver } +func (r *BucketResolver) SetResolveFunc(fn func(context.Context, string) (*cid.ID, error)) { + r.resolve = fn +} + func (r *BucketResolver) Resolve(ctx context.Context, name string) (*cid.ID, error) { cnrID, err := r.resolve(ctx, name) if err != nil {