mirror of
https://github.com/versity/versitygw.git
synced 2026-10-22 14:22:40 +00:00
The bucket default retention was never stored on objects: protection was computed at delete time from the live bucket rule (`CreatedAt` + period), so changing or removing the rule unlocked existing objects, `COMPLIANCE` ones included, and a rule set N days ago stopped protecting even new objects. The posix and azure backends now store the rule's retention, `{Mode, now + Days/Years}` truncated to milliseconds, as the object's own retention whenever a write sets no Object Lock parameters: `PutObject` (which also covers POST, RDMA and `CopyObject`), `CreateMultipartUpload` (the rule and start time are fixed at creation, as S3 does) and the unversioned in-place self-copy, which applied no lock settings before. As on S3, any explicit `x-amz-object-lock-legal-hold`, `ON` or `OFF`, means the default retention is not applied. The bucket-rule fallback in `checkObject` and `BucketLockConfig.CreatedAt` are removed, so objects written before this change lose bucket-derived protection, and an expired retention no longer skips the legal hold check.
`PutObjectLockConfiguration` now rejects a `DefaultRetention` with a mode but no `Days`/`Years` with `InvalidRequest` "DefaultRetention must specify a fixed retention (Days or Years) or a DefaultEventHold.", and a `<Rule>` without `DefaultRetention` with `MalformedXML`. Azure `CopyObject` with the `COPY` metadata directive no longer copies the source's `Objectretention`/`Objectlegalhold` metadata to the destination.
Browser POST uploads now validate and apply the `x-amz-object-lock-mode`, `x-amz-object-lock-retain-until-date` and `x-amz-object-lock-legal-hold` form fields. They require `s3:PutObjectRetention`/`s3:PutObjectLegalHold`, for anonymous uploads too, and return `ErrMissingObjectLockConfigurationNoSpaces` on buckets without Object Lock after authorization. A denied POST names the actions in S3's order: retention, legal hold, tagging, then `s3:PutObject`.
`NoSuchObjectLockConfiguration` now returns 404 without a trailing period. `x-amz-object-lock-retain-until-date` on HEAD and GET has millisecond precision and omits zero milliseconds, `GetObjectRetention` returns `<Retention>` with a `.000Z` date, and posix and azure `GetObject` now return the Object Lock headers.
375 lines
12 KiB
Go
375 lines
12 KiB
Go
// Copyright 2023 Versity Software
|
|
// This file is licensed under the Apache License, Version 2.0
|
|
// (the "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package integration
|
|
|
|
import (
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
"github.com/versity/versitygw/s3err"
|
|
)
|
|
|
|
func PutObjectLockConfiguration_non_existing_bucket(s *S3Conf) error {
|
|
testName := "PutObjectLockConfiguration_non_existing_bucket"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
|
|
Bucket: getPtr(getBucketName()),
|
|
ObjectLockConfiguration: &types.ObjectLockConfiguration{
|
|
ObjectLockEnabled: types.ObjectLockEnabledEnabled,
|
|
Rule: &types.ObjectLockRule{
|
|
DefaultRetention: &types.DefaultRetention{
|
|
Mode: types.ObjectLockRetentionModeCompliance,
|
|
Days: getPtr(int32(10)),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
cancel()
|
|
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func PutObjectLockConfiguration_empty_request_body(s *S3Conf) error {
|
|
testName := "PutObjectLockConfiguration_empty_request_body"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
|
|
Bucket: &bucket,
|
|
})
|
|
cancel()
|
|
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrMissingRequestBody)); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func PutObjectLockConfiguration_malformed_body(s *S3Conf) error {
|
|
testName := "PutObjectLockConfiguration_malformed_body"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
body := []byte("malformed_body")
|
|
hasher := md5.New()
|
|
_, err := hasher.Write(body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sum := hasher.Sum(nil)
|
|
md5Sum := base64.StdEncoding.EncodeToString(sum)
|
|
|
|
req, err := createSignedReq(
|
|
http.MethodPut,
|
|
s.endpoint,
|
|
fmt.Sprintf("%s?object-lock", bucket),
|
|
s.awsID,
|
|
s.awsSecret,
|
|
"s3",
|
|
s.awsRegion,
|
|
"",
|
|
body,
|
|
time.Now(),
|
|
map[string]string{"Content-Md5": md5Sum},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("err sending request: %w", err)
|
|
}
|
|
|
|
if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMalformedXML)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func PutObjectLockConfiguration_not_enabled_on_bucket_creation(s *S3Conf) error {
|
|
testName := "PutObjectLockConfiguration_not_enabled_on_bucket_creation"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
var days int32 = 12
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
|
|
Bucket: &bucket,
|
|
ObjectLockConfiguration: &types.ObjectLockConfiguration{
|
|
ObjectLockEnabled: types.ObjectLockEnabledEnabled,
|
|
Rule: &types.ObjectLockRule{
|
|
DefaultRetention: &types.DefaultRetention{
|
|
Days: &days,
|
|
Mode: types.ObjectLockRetentionModeCompliance,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
cancel()
|
|
// this test cases address the successful object lock status upload
|
|
// on versioning-disabled gateway mode, where versioning is not supported
|
|
// and object lock may be enabled without bucket versioning status check
|
|
// Note: this is not S3 compatible feature.
|
|
return err
|
|
})
|
|
}
|
|
|
|
func PutObjectLockConfiguration_invalid_status(s *S3Conf) error {
|
|
testName := "PutObjectLockConfiguration_invalid_status"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
var days int32 = 12
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
|
|
Bucket: &bucket,
|
|
ObjectLockConfiguration: &types.ObjectLockConfiguration{
|
|
ObjectLockEnabled: types.ObjectLockEnabled("invalid_status"),
|
|
Rule: &types.ObjectLockRule{
|
|
DefaultRetention: &types.DefaultRetention{
|
|
Days: &days,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
cancel()
|
|
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrMalformedXML)); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func PutObjectLockConfiguration_invalid_mode(s *S3Conf) error {
|
|
testName := "PutObjectLockConfiguration_invalid_status"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
var days int32 = 12
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
|
|
Bucket: &bucket,
|
|
ObjectLockConfiguration: &types.ObjectLockConfiguration{
|
|
ObjectLockEnabled: types.ObjectLockEnabledEnabled,
|
|
Rule: &types.ObjectLockRule{
|
|
DefaultRetention: &types.DefaultRetention{
|
|
Days: &days,
|
|
Mode: types.ObjectLockRetentionMode("invalid_mode"),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
cancel()
|
|
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrMalformedXML)); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func PutObjectLockConfiguration_both_years_and_days(s *S3Conf) error {
|
|
testName := "PutObjectLockConfiguration_both_years_and_days"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
var days, years int32 = 12, 24
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
|
|
Bucket: &bucket,
|
|
ObjectLockConfiguration: &types.ObjectLockConfiguration{
|
|
ObjectLockEnabled: types.ObjectLockEnabledEnabled,
|
|
Rule: &types.ObjectLockRule{
|
|
DefaultRetention: &types.DefaultRetention{
|
|
Days: &days,
|
|
Years: &years,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
cancel()
|
|
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrMalformedXML)); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func PutObjectLockConfiguration_invalid_years_days(s *S3Conf) error {
|
|
testName := "PutObjectLockConfiguration_invalid_years_days"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
putObjectLockConfiguration := func(retention *types.DefaultRetention) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
|
|
Bucket: &bucket,
|
|
ObjectLockConfiguration: &types.ObjectLockConfiguration{
|
|
ObjectLockEnabled: types.ObjectLockEnabledEnabled,
|
|
Rule: &types.ObjectLockRule{
|
|
DefaultRetention: retention,
|
|
},
|
|
},
|
|
})
|
|
cancel()
|
|
return err
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
retention *types.DefaultRetention
|
|
err s3err.InvalidArgumentError
|
|
}{
|
|
{
|
|
name: "negative days",
|
|
retention: &types.DefaultRetention{
|
|
Days: getPtr[int32](-3),
|
|
Mode: types.ObjectLockRetentionModeCompliance,
|
|
},
|
|
err: s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockRetentionDays, "-3"),
|
|
},
|
|
{
|
|
name: "too many days",
|
|
retention: &types.DefaultRetention{
|
|
Days: getPtr[int32](36501),
|
|
Mode: types.ObjectLockRetentionModeCompliance,
|
|
},
|
|
err: s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockRetentionDaysTooLarge, "36501"),
|
|
},
|
|
{
|
|
name: "negative years",
|
|
retention: &types.DefaultRetention{
|
|
Years: getPtr[int32](-5),
|
|
Mode: types.ObjectLockRetentionModeCompliance,
|
|
},
|
|
err: s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockRetentionYears, "-5"),
|
|
},
|
|
{
|
|
name: "too many years",
|
|
retention: &types.DefaultRetention{
|
|
Years: getPtr[int32](101),
|
|
Mode: types.ObjectLockRetentionModeCompliance,
|
|
},
|
|
err: s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockRetentionYearsTooLarge, "101"),
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
err := putObjectLockConfiguration(test.retention)
|
|
if err := checkApiErr(err, test.err); err != nil {
|
|
return fmt.Errorf("%s: %w", test.name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// PutObjectLockConfiguration_default_retention_missing_period covers a
|
|
// default retention rule with a mode but neither Days nor Years: it is
|
|
// rejected, and the bucket keeps the rule it had.
|
|
func PutObjectLockConfiguration_default_retention_missing_period(s *S3Conf) error {
|
|
testName := "PutObjectLockConfiguration_default_retention_missing_period"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
putObjectLockConfiguration := func(retention *types.DefaultRetention) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
|
|
Bucket: &bucket,
|
|
ObjectLockConfiguration: &types.ObjectLockConfiguration{
|
|
ObjectLockEnabled: types.ObjectLockEnabledEnabled,
|
|
Rule: &types.ObjectLockRule{
|
|
DefaultRetention: retention,
|
|
},
|
|
},
|
|
})
|
|
cancel()
|
|
return err
|
|
}
|
|
|
|
err := putObjectLockConfiguration(&types.DefaultRetention{
|
|
Mode: types.ObjectLockRetentionModeGovernance,
|
|
Days: getPtr(int32(1)),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = putObjectLockConfiguration(&types.DefaultRetention{
|
|
Mode: types.ObjectLockRetentionModeCompliance,
|
|
})
|
|
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLockDefaultRetentionPeriodRequired)); err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
out, err := s3client.GetObjectLockConfiguration(ctx, &s3.GetObjectLockConfigurationInput{
|
|
Bucket: &bucket,
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg := out.ObjectLockConfiguration
|
|
if cfg == nil || cfg.Rule == nil || cfg.Rule.DefaultRetention == nil {
|
|
return fmt.Errorf("expected the default retention rule to be kept, instead got %+v", cfg)
|
|
}
|
|
retention := cfg.Rule.DefaultRetention
|
|
if retention.Mode != types.ObjectLockRetentionModeGovernance || getInt32(retention.Days) != 1 || retention.Years != nil {
|
|
return fmt.Errorf("expected the default retention to stay %v for 1 day, instead got %v for %v days, %v years",
|
|
types.ObjectLockRetentionModeGovernance, retention.Mode, getInt32(retention.Days), getInt32(retention.Years))
|
|
}
|
|
|
|
return nil
|
|
}, withLock())
|
|
}
|
|
|
|
// PutObjectLockConfiguration_rule_without_default_retention covers a <Rule>
|
|
// element with no <DefaultRetention> in it.
|
|
func PutObjectLockConfiguration_rule_without_default_retention(s *S3Conf) error {
|
|
testName := "PutObjectLockConfiguration_rule_without_default_retention"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
|
|
Bucket: &bucket,
|
|
ObjectLockConfiguration: &types.ObjectLockConfiguration{
|
|
ObjectLockEnabled: types.ObjectLockEnabledEnabled,
|
|
Rule: &types.ObjectLockRule{},
|
|
},
|
|
})
|
|
cancel()
|
|
return checkApiErr(err, s3err.GetAPIError(s3err.ErrMalformedXML))
|
|
}, withLock())
|
|
}
|
|
|
|
func PutObjectLockConfiguration_success(s *S3Conf) error {
|
|
testName := "PutObjectLockConfiguration_success"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
|
|
Bucket: &bucket,
|
|
ObjectLockConfiguration: &types.ObjectLockConfiguration{
|
|
ObjectLockEnabled: types.ObjectLockEnabledEnabled,
|
|
},
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}, withLock())
|
|
}
|