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) }