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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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{}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
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
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
Loading…
Reference in a new issue