forked from TrueCloudLab/frostfs-s3-gw
parent
7d6271be8a
commit
6cf01bed14
12 changed files with 1078 additions and 253 deletions
|
@ -119,7 +119,8 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
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)
|
h.logAndSendError(w, "could not form object lock", reqInfo, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
139
api/handler/handlers_test.go
Normal file
139
api/handler/handlers_test.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -86,18 +86,18 @@ func (h *handler) GetBucketObjectLockConfigHandler(w http.ResponseWriter, r *htt
|
||||||
return
|
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 {
|
if !bktInfo.ObjectLockEnabled {
|
||||||
h.logAndSendError(w, "object lock disabled", reqInfo,
|
h.logAndSendError(w, "object lock disabled", reqInfo,
|
||||||
apiErrors.GetAPIError(apiErrors.ErrObjectLockConfigurationNotFound))
|
apiErrors.GetAPIError(apiErrors.ErrObjectLockConfigurationNotFound))
|
||||||
return
|
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 {
|
if settings.LockConfiguration == nil {
|
||||||
settings.LockConfiguration = &data.ObjectLockConfiguration{}
|
settings.LockConfiguration = &data.ObjectLockConfiguration{}
|
||||||
}
|
}
|
||||||
|
@ -174,6 +174,7 @@ func (h *handler) PutObjectLegalHoldHandler(w http.ResponseWriter, r *http.Reque
|
||||||
BktInfo: bktInfo,
|
BktInfo: bktInfo,
|
||||||
ObjName: objInfo.LegalHoldObject(),
|
ObjName: objInfo.LegalHoldObject(),
|
||||||
Lock: &data.ObjectLock{LegalHold: true},
|
Lock: &data.ObjectLock{LegalHold: true},
|
||||||
|
Metadata: make(map[string]string),
|
||||||
}
|
}
|
||||||
if _, err = h.obj.PutSystemObject(r.Context(), ps); err != nil {
|
if _, err = h.obj.PutSystemObject(r.Context(), ps); err != nil {
|
||||||
h.logAndSendError(w, "couldn't put legal hold", reqInfo, err)
|
h.logAndSendError(w, "couldn't put legal hold", reqInfo, err)
|
||||||
|
@ -278,8 +279,8 @@ func (h *handler) PutObjectRetentionHandler(w http.ResponseWriter, r *http.Reque
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if lockInfo != nil && lockInfo.Headers[layer.AttributeComplianceMode] != "" {
|
if err = checkLockInfo(lockInfo, r.Header); err != nil {
|
||||||
h.logAndSendError(w, "couldn't change compliance lock mode", reqInfo, err)
|
h.logAndSendError(w, "couldn't change lock mode", reqInfo, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,6 +288,7 @@ func (h *handler) PutObjectRetentionHandler(w http.ResponseWriter, r *http.Reque
|
||||||
BktInfo: bktInfo,
|
BktInfo: bktInfo,
|
||||||
ObjName: objInfo.RetentionObject(),
|
ObjName: objInfo.RetentionObject(),
|
||||||
Lock: lock,
|
Lock: lock,
|
||||||
|
Metadata: make(map[string]string),
|
||||||
}
|
}
|
||||||
if _, err = h.obj.PutSystemObject(r.Context(), ps); err != nil {
|
if _, err = h.obj.PutSystemObject(r.Context(), ps); err != nil {
|
||||||
h.logAndSendError(w, "couldn't put legal hold", reqInfo, err)
|
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) {
|
func (h *handler) GetObjectRetentionHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
reqInfo := api.GetReqInfo(r.Context())
|
reqInfo := api.GetReqInfo(r.Context())
|
||||||
|
|
||||||
|
@ -369,14 +387,16 @@ func checkLockConfiguration(conf *data.ObjectLockConfiguration) error {
|
||||||
return nil
|
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 !bktInfo.ObjectLockEnabled {
|
||||||
if existLockHeaders(header) {
|
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 {
|
if defaultConfig == nil {
|
||||||
defaultConfig = &data.ObjectLockConfiguration{}
|
defaultConfig = &data.ObjectLockConfiguration{}
|
||||||
}
|
}
|
||||||
|
@ -403,12 +423,12 @@ func formObjectLock(objectLock *data.ObjectLock, bktInfo *data.BucketInfo, defau
|
||||||
if until != "" {
|
if until != "" {
|
||||||
retentionDate, err := time.Parse(time.RFC3339, until)
|
retentionDate, err := time.Parse(time.RFC3339, until)
|
||||||
if err != nil {
|
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
|
objectLock.Until = retentionDate
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return objectLock, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func existLockHeaders(header http.Header) bool {
|
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) {
|
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 {
|
if retention.Mode != governanceMode && retention.Mode != complianceMode {
|
||||||
return nil, fmt.Errorf("invalid retention mode: %s", retention.Mode)
|
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,
|
IsCompliance: retention.Mode == complianceMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
if !lock.IsCompliance && !bypassGovernance {
|
|
||||||
return nil, fmt.Errorf("you cannot bypase governance mode")
|
|
||||||
}
|
|
||||||
|
|
||||||
return lock, nil
|
return lock, nil
|
||||||
}
|
}
|
||||||
|
|
632
api/handler/locking_test.go
Normal file
632
api/handler/locking_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -215,7 +215,8 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
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)
|
h.logAndSendError(w, "could not form object lock", reqInfo, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ type (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
attributeLocationConstraint = ".s3-location-constraint"
|
attributeLocationConstraint = ".s3-location-constraint"
|
||||||
attributeLockEnabled = "LockEnabled"
|
AttributeLockEnabled = "LockEnabled"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (n *layer) containerInfo(ctx context.Context, idCnr *cid.ID) (*data.BucketInfo, error) {
|
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)
|
info.Created = time.Unix(unix, 0)
|
||||||
case attributeLocationConstraint:
|
case attributeLocationConstraint:
|
||||||
info.LocationConstraint = val
|
info.LocationConstraint = val
|
||||||
case attributeLockEnabled:
|
case AttributeLockEnabled:
|
||||||
info.ObjectLockEnabled, err = strconv.ParseBool(val)
|
info.ObjectLockEnabled, err = strconv.ParseBool(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("could not parse container object lock enabled attribute",
|
log.Error("could not parse container object lock enabled attribute",
|
||||||
|
|
|
@ -3,6 +3,7 @@ package layer
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -201,8 +202,17 @@ func (n *layer) objectPut(ctx context.Context, bkt *data.BucketInfo, p *PutObjec
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.Lock != nil {
|
if p.Lock != nil {
|
||||||
// todo form lock system object
|
objInfo := &data.ObjectInfo{ID: oid, Name: p.Object}
|
||||||
// attributes = append(attributes, attributesFromLock(p.Lock)...)
|
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)
|
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
|
}, 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 {
|
func updateCRDT2PSetHeaders(header map[string]string, versions *objectVersions, versioningEnabled bool) []*oid.ID {
|
||||||
if !versioningEnabled {
|
if !versioningEnabled {
|
||||||
header[versionsUnversionedAttr] = "true"
|
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 {
|
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 {
|
if err != nil {
|
||||||
n.log.Warn("couldn't get versioning settings object", zap.Error(err))
|
n.log.Warn("couldn't get versioning settings object", zap.Error(err))
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -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) {
|
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
|
return settings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,7 +236,7 @@ func (n *layer) GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo)
|
||||||
return nil, err
|
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",
|
n.log.Warn("couldn't put system meta to objects cache",
|
||||||
zap.Stringer("object id", obj.ID()),
|
zap.Stringer("object id", obj.ID()),
|
||||||
zap.Stringer("bucket id", bktInfo.CID),
|
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)
|
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))
|
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 {
|
func attributesFromLock(lock *data.ObjectLock) []*object.Attribute {
|
||||||
var result []*object.Attribute
|
var result []*object.Attribute
|
||||||
if !lock.LegalHold {
|
if !lock.Until.IsZero() {
|
||||||
attrRetainUntil := object.NewAttribute()
|
attrRetainUntil := object.NewAttribute()
|
||||||
attrRetainUntil.SetKey(AttributeRetainUntil)
|
attrRetainUntil.SetKey(AttributeRetainUntil)
|
||||||
attrRetainUntil.SetValue(lock.Until.Format(time.RFC3339))
|
attrRetainUntil.SetValue(lock.Until.Format(time.RFC3339))
|
||||||
|
|
|
@ -300,7 +300,7 @@ func (n *layer) PutBucketVersioning(ctx context.Context, p *PutSettingsParams) (
|
||||||
Reader: nil,
|
Reader: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
return n.putSystemObject(ctx, s)
|
return n.PutSystemObject(ctx, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *layer) GetBucketVersioning(ctx context.Context, bucketName string) (*data.BucketSettings, error) {
|
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 nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return n.getBucketSettings(ctx, bktInfo)
|
return n.GetBucketSettings(ctx, bktInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *layer) ListObjectVersions(ctx context.Context, p *ListObjectVersionsParams) (*ListObjectVersionsInfo, error) {
|
func (n *layer) ListObjectVersions(ctx context.Context, p *ListObjectVersionsParams) (*ListObjectVersionsInfo, error) {
|
||||||
|
@ -398,15 +398,6 @@ func contains(list []string, elem string) bool {
|
||||||
return false
|
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 {
|
func objectInfoToBucketSettings(info *data.ObjectInfo) *data.BucketSettings {
|
||||||
res := &data.BucketSettings{}
|
res := &data.BucketSettings{}
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,6 @@ package layer
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -14,207 +10,18 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
"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"
|
||||||
"github.com/nspcc-dev/neofs-s3-gw/api/data"
|
"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-s3-gw/creds/accessbox"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/container"
|
|
||||||
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
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/logger"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/object"
|
"github.com/nspcc-dev/neofs-sdk-go/object"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/object/address"
|
"github.com/nspcc-dev/neofs-sdk-go/object/address"
|
||||||
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
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/object/id/test"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/owner"
|
|
||||||
tokentest "github.com/nspcc-dev/neofs-sdk-go/token/test"
|
tokentest "github.com/nspcc-dev/neofs-sdk-go/token/test"
|
||||||
"github.com/stretchr/testify/require"
|
"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 {
|
func (tc *testContext) putObject(content []byte) *data.ObjectInfo {
|
||||||
objInfo, err := tc.layer.PutObject(tc.ctx, &PutObjectParams{
|
objInfo, err := tc.layer.PutObject(tc.ctx, &PutObjectParams{
|
||||||
Bucket: tc.bktID.String(),
|
Bucket: tc.bktID.String(),
|
||||||
|
@ -307,7 +114,7 @@ func (tc *testContext) checkListObjects(ids ...*oid.ID) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *testContext) getSystemObject(objectName string) *object.Object {
|
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() {
|
for _, attr := range obj.Attributes() {
|
||||||
if attr.Key() == objectSystemAttributeName && attr.Value() == objectName {
|
if attr.Key() == objectSystemAttributeName && attr.Value() == objectName {
|
||||||
return obj
|
return obj
|
||||||
|
@ -325,7 +132,7 @@ type testContext struct {
|
||||||
bktID *cid.ID
|
bktID *cid.ID
|
||||||
bktInfo *data.BucketInfo
|
bktInfo *data.BucketInfo
|
||||||
obj string
|
obj string
|
||||||
testNeoFS *testNeoFS
|
testNeoFS *mock.TestNeoFS
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareContext(t *testing.T, cachesConfig ...*CachesConfig) *testContext {
|
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"))
|
l, err := logger.New(logger.WithTraceLevel("panic"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
tp := newTestPool()
|
tp := mock.NewTestPool()
|
||||||
|
|
||||||
bktName := "testbucket1"
|
bktName := "testbucket1"
|
||||||
bktID, err := tp.CreateContainer(ctx, PrmContainerCreate{
|
bktID, err := tp.CreateContainer(ctx, PrmContainerCreate{
|
||||||
|
@ -821,7 +628,7 @@ func TestSystemObjectsVersioning(t *testing.T) {
|
||||||
addr.SetObjectID(objMeta.ID())
|
addr.SetObjectID(objMeta.ID())
|
||||||
|
|
||||||
// simulate failed deletion
|
// simulate failed deletion
|
||||||
tc.testNeoFS.objects[addr.String()] = objMeta
|
tc.testNeoFS.Objects[addr.String()] = objMeta
|
||||||
|
|
||||||
bktInfo := &data.BucketInfo{
|
bktInfo := &data.BucketInfo{
|
||||||
Name: tc.bkt,
|
Name: tc.bkt,
|
||||||
|
@ -853,7 +660,7 @@ func TestDeleteSystemObjectsVersioning(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// simulate failed deletion
|
// 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)
|
tagging, err := tc.layer.GetBucketTagging(tc.ctx, tc.bkt)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
216
api/mock/sdk_pool_mock.go
Normal file
216
api/mock/sdk_pool_mock.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -35,6 +35,10 @@ type BucketResolver struct {
|
||||||
next *BucketResolver
|
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) {
|
func (r *BucketResolver) Resolve(ctx context.Context, name string) (*cid.ID, error) {
|
||||||
cnrID, err := r.resolve(ctx, name)
|
cnrID, err := r.resolve(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in a new issue