frostfs-s3-gw/api/handler/locking_test.go
Denis Kirillov 9875307c9b [#556] Check bucket name not only during creation
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-11-20 08:13:27 +00:00

638 lines
20 KiB
Go

package handler
import (
"bytes"
"context"
"encoding/xml"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"time"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/encryption"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"github.com/stretchr/testify/require"
)
const defaultURL = "http://localhost/"
func TestFormObjectLock(t *testing.T) {
ctx := context.Background()
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{Retention: &data.RetentionLock{
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{Retention: &data.RetentionLock{
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().Add(time.Minute).Format(time.RFC3339)},
api.AmzObjectLockMode: {governanceMode},
api.AmzObjectLockLegalHold: {legalHoldOn},
},
expectedLock: &data.ObjectLock{
LegalHold: &data.LegalHoldLock{Enabled: true},
Retention: &data.RetentionLock{Until: time.Now().Add(time.Minute)}},
},
{
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.RFC822)},
},
expectedError: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
actualObjLock, err := formObjectLock(ctx, 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) {
ctx := context.Background()
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().Add(time.Minute).Format(time.RFC3339),
},
expectedLock: &data.ObjectLock{Retention: &data.RetentionLock{
Until: time.Now().Add(time.Minute),
IsCompliance: true}},
},
{
name: "basic governance",
retention: &data.Retention{
Mode: governanceMode,
RetainUntilDate: time.Now().Add(time.Minute).Format(time.RFC3339),
},
header: map[string][]string{
api.AmzBypassGovernanceRetention: {strconv.FormatBool(true)},
},
expectedLock: &data.ObjectLock{Retention: &data.RetentionLock{Until: time.Now().Add(time.Minute)}},
},
{
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(ctx, 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)
if expected.Retention != nil {
require.Equal(t, expected.Retention.IsCompliance, actual.Retention.IsCompliance)
require.InDelta(t, expected.Retention.Until.Unix(), actual.Retention.Until.Unix(), 1)
}
}
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(hc, bktLockDisabled)
bktLockEnabled := "bucket-lock-enabled"
createTestBucketWithLock(hc, bktLockEnabled, nil)
bktLockEnabledWithOldConfig := "bucket-lock-enabled-old-conf"
createTestBucketWithLock(hc, bktLockEnabledWithOldConfig,
&data.ObjectLockConfiguration{
Rule: &data.ObjectLockRule{
DefaultRetention: &data.DefaultRetention{
Days: 1,
Mode: complianceMode,
},
},
})
for _, tc := range []struct {
name string
bucket string
expectedError apierr.Error
noError bool
configuration *data.ObjectLockConfiguration
}{
{
name: "bkt not found",
bucket: "not-found-bucket",
expectedError: apierr.GetAPIError(apierr.ErrNoSuchBucket),
},
{
name: "bkt lock disabled",
bucket: bktLockDisabled,
expectedError: apierr.GetAPIError(apierr.ErrObjectLockConfigurationNotAllowed),
},
{
name: "invalid configuration",
bucket: bktLockEnabled,
expectedError: apierr.GetAPIError(apierr.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(middleware.SetReqInfo(r.Context(), middleware.NewReqInfo(w, r, middleware.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) {
hc := prepareHandlerContext(t)
bktLockDisabled := "bucket-lock-disabled"
createTestBucket(hc, bktLockDisabled)
bktLockEnabled := "bucket-lock-enabled"
createTestBucketWithLock(hc, bktLockEnabled, nil)
oldConfig := &data.ObjectLockConfiguration{
Rule: &data.ObjectLockRule{
DefaultRetention: &data.DefaultRetention{
Days: 1,
Mode: complianceMode,
},
},
}
bktLockEnabledWithOldConfig := "bucket-lock-enabled-old-conf"
createTestBucketWithLock(hc, bktLockEnabledWithOldConfig, oldConfig)
for _, tc := range []struct {
name string
bucket string
expectedError apierr.Error
noError bool
expectedConf *data.ObjectLockConfiguration
}{
{
name: "bkt not found",
bucket: "not-found-bucket",
expectedError: apierr.GetAPIError(apierr.ErrNoSuchBucket),
},
{
name: "bkt lock disabled",
bucket: bktLockDisabled,
expectedError: apierr.GetAPIError(apierr.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(middleware.SetReqInfo(r.Context(), middleware.NewReqInfo(w, r, middleware.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 apierr.Error) {
actualErrorResponse := &middleware.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 != apierr.ErrInternalError {
require.Equal(t, expectedError.Description, actualErrorResponse.Message)
}
}
func TestObjectLegalHold(t *testing.T) {
hc := prepareHandlerContext(t)
bktName := "bucket-lock-enabled"
bktInfo := createTestBucketWithLock(hc, bktName, nil)
objName := "obj-for-legal-hold"
createTestObject(hc, bktInfo, objName, encryption.Params{})
getObjectLegalHold(hc, bktName, objName, legalHoldOff)
putObjectLegalHold(hc, bktName, objName, legalHoldOn)
getObjectLegalHold(hc, bktName, objName, legalHoldOn)
// to make sure put hold is an idempotent operation
putObjectLegalHold(hc, bktName, objName, legalHoldOn)
putObjectLegalHold(hc, bktName, objName, legalHoldOff)
getObjectLegalHold(hc, bktName, objName, legalHoldOff)
// to make sure put hold is an idempotent operation
putObjectLegalHold(hc, bktName, objName, legalHoldOff)
}
func getObjectLegalHold(hc *handlerContext, bktName, objName, status string) {
w, r := prepareTestRequest(hc, bktName, objName, nil)
hc.Handler().GetObjectLegalHoldHandler(w, r)
assertLegalHold(hc.t, w, status)
}
func putObjectLegalHold(hc *handlerContext, bktName, objName, status string) {
w, r := prepareTestRequest(hc, bktName, objName, &data.LegalHold{Status: status})
hc.Handler().PutObjectLegalHoldHandler(w, r)
assertStatus(hc.t, w, http.StatusOK)
}
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) {
hc := prepareHandlerContext(t)
bktName := "bucket-lock-enabled"
bktInfo := createTestBucketWithLock(hc, bktName, nil)
objName := "obj-for-retention"
createTestObject(hc, bktInfo, objName, encryption.Params{})
getObjectRetention(hc, bktName, objName, nil, apierr.ErrNoSuchKey)
retention := &data.Retention{Mode: governanceMode, RetainUntilDate: time.Now().Add(time.Minute).UTC().Format(time.RFC3339)}
putObjectRetention(hc, bktName, objName, retention, false, 0)
getObjectRetention(hc, bktName, objName, retention, 0)
retention = &data.Retention{Mode: governanceMode, RetainUntilDate: time.Now().UTC().Add(time.Minute).Format(time.RFC3339)}
putObjectRetention(hc, bktName, objName, retention, false, apierr.ErrInternalError)
retention = &data.Retention{Mode: complianceMode, RetainUntilDate: time.Now().Add(time.Minute).UTC().Format(time.RFC3339)}
putObjectRetention(hc, bktName, objName, retention, true, 0)
getObjectRetention(hc, bktName, objName, retention, 0)
putObjectRetention(hc, bktName, objName, retention, true, apierr.ErrInternalError)
}
func getObjectRetention(hc *handlerContext, bktName, objName string, retention *data.Retention, errCode apierr.ErrorCode) {
w, r := prepareTestRequest(hc, bktName, objName, nil)
hc.Handler().GetObjectRetentionHandler(w, r)
if errCode == 0 {
assertRetention(hc.t, w, retention)
} else {
assertS3Error(hc.t, w, apierr.GetAPIError(errCode))
}
}
func putObjectRetention(hc *handlerContext, bktName, objName string, retention *data.Retention, byPass bool, errCode apierr.ErrorCode) {
w, r := prepareTestRequest(hc, bktName, objName, retention)
if byPass {
r.Header.Set(api.AmzBypassGovernanceRetention, strconv.FormatBool(true))
}
hc.Handler().PutObjectRetentionHandler(w, r)
if errCode == 0 {
assertStatus(hc.t, w, http.StatusOK)
} else {
assertS3Error(hc.t, w, apierr.GetAPIError(errCode))
}
}
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) {
hc := prepareHandlerContext(t)
bktName := "bucket-lock-enabled"
lockConfig := &data.ObjectLockConfiguration{
ObjectLockEnabled: enabledValue,
Rule: &data.ObjectLockRule{
DefaultRetention: &data.DefaultRetention{
Days: 1,
Mode: governanceMode,
},
},
}
createTestBucketWithLock(hc, bktName, lockConfig)
objDefault := "obj-default-retention"
putObject(hc, bktName, objDefault)
getObjectRetentionApproximate(hc, bktName, objDefault, governanceMode, time.Now().Add(24*time.Hour))
getObjectLegalHold(hc, bktName, objDefault, legalHoldOff)
objOverride := "obj-override-retention"
w, r := prepareTestRequest(hc, bktName, objOverride, nil)
r.Header.Set(api.AmzObjectLockMode, complianceMode)
r.Header.Set(api.AmzObjectLockLegalHold, legalHoldOn)
r.Header.Set(api.AmzBypassGovernanceRetention, "true")
r.Header.Set(api.AmzObjectLockRetainUntilDate, time.Now().Add(2*24*time.Hour).Format(time.RFC3339))
hc.Handler().PutObjectHandler(w, r)
assertStatus(t, w, http.StatusOK)
getObjectRetentionApproximate(hc, bktName, objOverride, complianceMode, time.Now().Add(2*24*time.Hour))
getObjectLegalHold(hc, bktName, objOverride, legalHoldOn)
}
func getObjectRetentionApproximate(hc *handlerContext, bktName, objName, mode string, untilDate time.Time) {
w, r := prepareTestRequest(hc, bktName, objName, nil)
hc.Handler().GetObjectRetentionHandler(w, r)
expectedRetention := &data.Retention{
Mode: mode,
RetainUntilDate: untilDate.Format(time.RFC3339),
}
assertRetentionApproximate(hc.t, w, expectedRetention, 1)
}
func TestPutLockErrors(t *testing.T) {
hc := prepareHandlerContext(t)
bktName, objName := "bucket-lock-enabled", "object"
createTestBucketWithLock(hc, bktName, nil)
headers := map[string]string{api.AmzObjectLockMode: complianceMode}
putObjectWithLockFailed(t, hc, bktName, objName, headers, apierr.ErrObjectLockInvalidHeaders)
delete(headers, api.AmzObjectLockMode)
headers[api.AmzObjectLockRetainUntilDate] = time.Now().Add(time.Minute).Format(time.RFC3339)
putObjectWithLockFailed(t, hc, bktName, objName, headers, apierr.ErrObjectLockInvalidHeaders)
headers[api.AmzObjectLockMode] = "dummy"
putObjectWithLockFailed(t, hc, bktName, objName, headers, apierr.ErrUnknownWORMModeDirective)
headers[api.AmzObjectLockMode] = complianceMode
headers[api.AmzObjectLockRetainUntilDate] = time.Now().Format(time.RFC3339)
putObjectWithLockFailed(t, hc, bktName, objName, headers, apierr.ErrPastObjectLockRetainDate)
headers[api.AmzObjectLockRetainUntilDate] = "dummy"
putObjectWithLockFailed(t, hc, bktName, objName, headers, apierr.ErrInvalidRetentionDate)
putObject(hc, bktName, objName)
retention := &data.Retention{Mode: governanceMode}
putObjectRetentionFailed(t, hc, bktName, objName, retention, apierr.ErrMalformedXML)
retention.Mode = "dummy"
retention.RetainUntilDate = time.Now().Add(time.Minute).UTC().Format(time.RFC3339)
putObjectRetentionFailed(t, hc, bktName, objName, retention, apierr.ErrMalformedXML)
retention.Mode = governanceMode
retention.RetainUntilDate = time.Now().UTC().Format(time.RFC3339)
putObjectRetentionFailed(t, hc, bktName, objName, retention, apierr.ErrPastObjectLockRetainDate)
}
func putObjectWithLockFailed(t *testing.T, hc *handlerContext, bktName, objName string, headers map[string]string, errCode apierr.ErrorCode) {
w, r := prepareTestRequest(hc, bktName, objName, nil)
for key, val := range headers {
r.Header.Set(key, val)
}
hc.Handler().PutObjectHandler(w, r)
assertS3Error(t, w, apierr.GetAPIError(errCode))
}
func putObjectRetentionFailed(t *testing.T, hc *handlerContext, bktName, objName string, retention *data.Retention, errCode apierr.ErrorCode) {
w, r := prepareTestRequest(hc, bktName, objName, retention)
hc.Handler().PutObjectRetentionHandler(w, r)
assertS3Error(t, w, apierr.GetAPIError(errCode))
}
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)
}