Add support for object locking with legal hold. (#8634)
parent
ba758361b3
commit
60e60f68dd
@ -0,0 +1,521 @@ |
||||
/* |
||||
* MinIO Cloud Storage, (C) 2020 MinIO, Inc. |
||||
* |
||||
* 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 objectlock |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/xml" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/beevik/ntp" |
||||
xhttp "github.com/minio/minio/cmd/http" |
||||
"github.com/minio/minio/cmd/logger" |
||||
"github.com/minio/minio/pkg/env" |
||||
) |
||||
|
||||
// Mode - object retention mode.
|
||||
type Mode string |
||||
|
||||
const ( |
||||
// Governance - governance mode.
|
||||
Governance Mode = "GOVERNANCE" |
||||
|
||||
// Compliance - compliance mode.
|
||||
Compliance Mode = "COMPLIANCE" |
||||
|
||||
// Invalid - invalid retention mode.
|
||||
Invalid Mode = "" |
||||
) |
||||
|
||||
func parseMode(modeStr string) (mode Mode) { |
||||
switch strings.ToUpper(modeStr) { |
||||
case "GOVERNANCE": |
||||
mode = Governance |
||||
case "COMPLIANCE": |
||||
mode = Compliance |
||||
default: |
||||
mode = Invalid |
||||
} |
||||
return mode |
||||
} |
||||
|
||||
// LegalHoldStatus - object legal hold status.
|
||||
type LegalHoldStatus string |
||||
|
||||
const ( |
||||
// ON -legal hold is on.
|
||||
ON LegalHoldStatus = "ON" |
||||
|
||||
// OFF -legal hold is off.
|
||||
OFF LegalHoldStatus = "OFF" |
||||
) |
||||
|
||||
func parseLegalHoldStatus(holdStr string) LegalHoldStatus { |
||||
switch strings.ToUpper(holdStr) { |
||||
case "ON": |
||||
return ON |
||||
case "OFF": |
||||
return OFF |
||||
} |
||||
return LegalHoldStatus("") |
||||
} |
||||
|
||||
var ( |
||||
// ErrMalformedBucketObjectConfig -indicates that the bucket object lock config is malformed
|
||||
ErrMalformedBucketObjectConfig = errors.New("invalid bucket object lock config") |
||||
// ErrInvalidRetentionDate - indicates that retention date needs to be in ISO 8601 format
|
||||
ErrInvalidRetentionDate = errors.New("date must be provided in ISO 8601 format") |
||||
// ErrPastObjectLockRetainDate - indicates that retention date must be in the future
|
||||
ErrPastObjectLockRetainDate = errors.New("the retain until date must be in the future") |
||||
// ErrUnknownWORMModeDirective - indicates that the retention mode is invalid
|
||||
ErrUnknownWORMModeDirective = errors.New("unknown WORM mode directive") |
||||
// ErrObjectLockMissingContentMD5 - indicates missing Content-MD5 header for put object requests with locking
|
||||
ErrObjectLockMissingContentMD5 = errors.New("content-MD5 HTTP header is required for Put Object requests with Object Lock parameters") |
||||
// ErrObjectLockInvalidHeaders indicates that object lock headers are missing
|
||||
ErrObjectLockInvalidHeaders = errors.New("x-amz-object-lock-retain-until-date and x-amz-object-lock-mode must both be supplied") |
||||
// ErrMalformedXML - generic error indicating malformed XML
|
||||
ErrMalformedXML = errors.New("the XML you provided was not well-formed or did not validate against our published schema") |
||||
) |
||||
|
||||
const ( |
||||
ntpServerEnv = "MINIO_NTP_SERVER" |
||||
) |
||||
|
||||
var ( |
||||
ntpServer = env.Get(ntpServerEnv, "") |
||||
) |
||||
|
||||
// UTCNowNTP - is similar in functionality to UTCNow()
|
||||
// but only used when we do not wish to rely on system
|
||||
// time.
|
||||
func UTCNowNTP() (time.Time, error) { |
||||
// ntp server is disabled
|
||||
if ntpServer == "" { |
||||
return time.Now().UTC(), nil |
||||
} |
||||
return ntp.Time(ntpServer) |
||||
} |
||||
|
||||
// Retention - bucket level retention configuration.
|
||||
type Retention struct { |
||||
Mode Mode |
||||
Validity time.Duration |
||||
} |
||||
|
||||
// IsEmpty - returns whether retention is empty or not.
|
||||
func (r Retention) IsEmpty() bool { |
||||
return r.Mode == "" || r.Validity == 0 |
||||
} |
||||
|
||||
// Retain - check whether given date is retainable by validity time.
|
||||
func (r Retention) Retain(created time.Time) bool { |
||||
t, err := UTCNowNTP() |
||||
if err != nil { |
||||
logger.LogIf(context.Background(), err) |
||||
// Retain
|
||||
return true |
||||
} |
||||
return created.Add(r.Validity).After(t) |
||||
} |
||||
|
||||
// BucketObjectLockConfig - map of bucket and retention configuration.
|
||||
type BucketObjectLockConfig struct { |
||||
sync.RWMutex |
||||
retentionMap map[string]Retention |
||||
} |
||||
|
||||
// Set - set retention configuration.
|
||||
func (config *BucketObjectLockConfig) Set(bucketName string, retention Retention) { |
||||
config.Lock() |
||||
config.retentionMap[bucketName] = retention |
||||
config.Unlock() |
||||
} |
||||
|
||||
// Get - Get retention configuration.
|
||||
func (config *BucketObjectLockConfig) Get(bucketName string) (r Retention, ok bool) { |
||||
config.RLock() |
||||
defer config.RUnlock() |
||||
r, ok = config.retentionMap[bucketName] |
||||
return r, ok |
||||
} |
||||
|
||||
// Remove - removes retention configuration.
|
||||
func (config *BucketObjectLockConfig) Remove(bucketName string) { |
||||
config.Lock() |
||||
delete(config.retentionMap, bucketName) |
||||
config.Unlock() |
||||
} |
||||
|
||||
// NewBucketObjectLockConfig returns initialized BucketObjectLockConfig
|
||||
func NewBucketObjectLockConfig() *BucketObjectLockConfig { |
||||
return &BucketObjectLockConfig{ |
||||
retentionMap: map[string]Retention{}, |
||||
} |
||||
} |
||||
|
||||
// DefaultRetention - default retention configuration.
|
||||
type DefaultRetention struct { |
||||
XMLName xml.Name `xml:"DefaultRetention"` |
||||
Mode Mode `xml:"Mode"` |
||||
Days *uint64 `xml:"Days"` |
||||
Years *uint64 `xml:"Years"` |
||||
} |
||||
|
||||
// Maximum support retention days and years supported by AWS S3.
|
||||
const ( |
||||
// This tested by using `mc lock` command
|
||||
maximumRetentionDays = 36500 |
||||
maximumRetentionYears = 100 |
||||
) |
||||
|
||||
// UnmarshalXML - decodes XML data.
|
||||
func (dr *DefaultRetention) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { |
||||
// Make subtype to avoid recursive UnmarshalXML().
|
||||
type defaultRetention DefaultRetention |
||||
retention := defaultRetention{} |
||||
|
||||
if err := d.DecodeElement(&retention, &start); err != nil { |
||||
return err |
||||
} |
||||
|
||||
switch string(retention.Mode) { |
||||
case "GOVERNANCE", "COMPLIANCE": |
||||
default: |
||||
return fmt.Errorf("unknown retention mode %v", retention.Mode) |
||||
} |
||||
|
||||
if retention.Days == nil && retention.Years == nil { |
||||
return fmt.Errorf("either Days or Years must be specified") |
||||
} |
||||
|
||||
if retention.Days != nil && retention.Years != nil { |
||||
return fmt.Errorf("either Days or Years must be specified, not both") |
||||
} |
||||
|
||||
if retention.Days != nil { |
||||
if *retention.Days == 0 { |
||||
return fmt.Errorf("Default retention period must be a positive integer value for 'Days'") |
||||
} |
||||
if *retention.Days > maximumRetentionDays { |
||||
return fmt.Errorf("Default retention period too large for 'Days' %d", *retention.Days) |
||||
} |
||||
} else if *retention.Years == 0 { |
||||
return fmt.Errorf("Default retention period must be a positive integer value for 'Years'") |
||||
} else if *retention.Years > maximumRetentionYears { |
||||
return fmt.Errorf("Default retention period too large for 'Years' %d", *retention.Years) |
||||
} |
||||
|
||||
*dr = DefaultRetention(retention) |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Config - object lock configuration specified in
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/Type_API_ObjectLockConfiguration.html
|
||||
type Config struct { |
||||
XMLNS string `xml:"xmlns,attr,omitempty"` |
||||
XMLName xml.Name `xml:"ObjectLockConfiguration"` |
||||
ObjectLockEnabled string `xml:"ObjectLockEnabled"` |
||||
Rule *struct { |
||||
DefaultRetention DefaultRetention `xml:"DefaultRetention"` |
||||
} `xml:"Rule,omitempty"` |
||||
} |
||||
|
||||
// UnmarshalXML - decodes XML data.
|
||||
func (config *Config) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { |
||||
// Make subtype to avoid recursive UnmarshalXML().
|
||||
type objectLockConfig Config |
||||
parsedConfig := objectLockConfig{} |
||||
|
||||
if err := d.DecodeElement(&parsedConfig, &start); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if parsedConfig.ObjectLockEnabled != "Enabled" { |
||||
return fmt.Errorf("only 'Enabled' value is allowd to ObjectLockEnabled element") |
||||
} |
||||
|
||||
*config = Config(parsedConfig) |
||||
return nil |
||||
} |
||||
|
||||
// ToRetention - convert to Retention type.
|
||||
func (config *Config) ToRetention() (r Retention) { |
||||
if config.Rule != nil { |
||||
r.Mode = config.Rule.DefaultRetention.Mode |
||||
|
||||
t, err := UTCNowNTP() |
||||
if err != nil { |
||||
logger.LogIf(context.Background(), err) |
||||
// Do not change any configuration
|
||||
// upon NTP failure.
|
||||
return r |
||||
} |
||||
|
||||
if config.Rule.DefaultRetention.Days != nil { |
||||
r.Validity = t.AddDate(0, 0, int(*config.Rule.DefaultRetention.Days)).Sub(t) |
||||
} else { |
||||
r.Validity = t.AddDate(int(*config.Rule.DefaultRetention.Years), 0, 0).Sub(t) |
||||
} |
||||
} |
||||
|
||||
return r |
||||
} |
||||
|
||||
// ParseObjectLockConfig parses ObjectLockConfig from xml
|
||||
func ParseObjectLockConfig(reader io.Reader) (*Config, error) { |
||||
config := Config{} |
||||
if err := xml.NewDecoder(reader).Decode(&config); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &config, nil |
||||
} |
||||
|
||||
// NewObjectLockConfig returns a initialized objectlock.Config struct
|
||||
func NewObjectLockConfig() *Config { |
||||
return &Config{ |
||||
ObjectLockEnabled: "Enabled", |
||||
} |
||||
} |
||||
|
||||
// RetentionDate is a embedded type containing time.Time to unmarshal
|
||||
// Date in Retention
|
||||
type RetentionDate struct { |
||||
time.Time |
||||
} |
||||
|
||||
// UnmarshalXML parses date from Retention and validates date format
|
||||
func (rDate *RetentionDate) UnmarshalXML(d *xml.Decoder, startElement xml.StartElement) error { |
||||
var dateStr string |
||||
err := d.DecodeElement(&dateStr, &startElement) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
// While AWS documentation mentions that the date specified
|
||||
// must be present in ISO 8601 format, in reality they allow
|
||||
// users to provide RFC 3339 compliant dates.
|
||||
retDate, err := time.Parse(time.RFC3339, dateStr) |
||||
if err != nil { |
||||
return ErrInvalidRetentionDate |
||||
} |
||||
|
||||
*rDate = RetentionDate{retDate} |
||||
return nil |
||||
} |
||||
|
||||
// MarshalXML encodes expiration date if it is non-zero and encodes
|
||||
// empty string otherwise
|
||||
func (rDate *RetentionDate) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error { |
||||
if *rDate == (RetentionDate{time.Time{}}) { |
||||
return nil |
||||
} |
||||
return e.EncodeElement(rDate.Format(time.RFC3339), startElement) |
||||
} |
||||
|
||||
// ObjectRetention specified in
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectRetention.html
|
||||
type ObjectRetention struct { |
||||
XMLNS string `xml:"xmlns,attr,omitempty"` |
||||
XMLName xml.Name `xml:"Retention"` |
||||
Mode Mode `xml:"Mode,omitempty"` |
||||
RetainUntilDate RetentionDate `xml:"RetainUntilDate,omitempty"` |
||||
} |
||||
|
||||
// ParseObjectRetention constructs ObjectRetention struct from xml input
|
||||
func ParseObjectRetention(reader io.Reader) (*ObjectRetention, error) { |
||||
ret := ObjectRetention{} |
||||
if err := xml.NewDecoder(reader).Decode(&ret); err != nil { |
||||
return nil, err |
||||
} |
||||
if ret.Mode != Compliance && ret.Mode != Governance { |
||||
return &ret, ErrUnknownWORMModeDirective |
||||
} |
||||
|
||||
t, err := UTCNowNTP() |
||||
if err != nil { |
||||
logger.LogIf(context.Background(), err) |
||||
return &ret, ErrPastObjectLockRetainDate |
||||
} |
||||
|
||||
if ret.RetainUntilDate.Before(t) { |
||||
return &ret, ErrPastObjectLockRetainDate |
||||
} |
||||
|
||||
return &ret, nil |
||||
} |
||||
|
||||
// IsObjectLockRetentionRequested returns true if object lock retention headers are set.
|
||||
func IsObjectLockRetentionRequested(h http.Header) bool { |
||||
if _, ok := h[xhttp.AmzObjectLockMode]; ok { |
||||
return true |
||||
} |
||||
if _, ok := h[xhttp.AmzObjectLockRetainUntilDate]; ok { |
||||
return true |
||||
} |
||||
return false |
||||
} |
||||
|
||||
// IsObjectLockLegalHoldRequested returns true if object lock legal hold header is set.
|
||||
func IsObjectLockLegalHoldRequested(h http.Header) bool { |
||||
_, ok := h[xhttp.AmzObjectLockLegalHold] |
||||
return ok |
||||
} |
||||
|
||||
// IsObjectLockGovernanceBypassSet returns true if object lock governance bypass header is set.
|
||||
func IsObjectLockGovernanceBypassSet(h http.Header) bool { |
||||
return strings.ToLower(h.Get(xhttp.AmzObjectLockBypassGovernance)) == "true" |
||||
} |
||||
|
||||
// IsObjectLockRequested returns true if legal hold or object lock retention headers are requested.
|
||||
func IsObjectLockRequested(h http.Header) bool { |
||||
return IsObjectLockLegalHoldRequested(h) || IsObjectLockRetentionRequested(h) |
||||
} |
||||
|
||||
// ParseObjectLockRetentionHeaders parses http headers to extract retention mode and retention date
|
||||
func ParseObjectLockRetentionHeaders(h http.Header) (rmode Mode, r RetentionDate, err error) { |
||||
retMode := h.Get(xhttp.AmzObjectLockMode) |
||||
dateStr := h.Get(xhttp.AmzObjectLockRetainUntilDate) |
||||
if len(retMode) == 0 || len(dateStr) == 0 { |
||||
return rmode, r, ErrObjectLockInvalidHeaders |
||||
} |
||||
rmode = parseMode(retMode) |
||||
if rmode == Invalid { |
||||
return rmode, r, ErrUnknownWORMModeDirective |
||||
} |
||||
|
||||
var retDate time.Time |
||||
// While AWS documentation mentions that the date specified
|
||||
// must be present in ISO 8601 format, in reality they allow
|
||||
// users to provide RFC 3339 compliant dates.
|
||||
retDate, err = time.Parse(time.RFC3339, dateStr) |
||||
if err != nil { |
||||
return rmode, r, ErrInvalidRetentionDate |
||||
} |
||||
|
||||
t, err := UTCNowNTP() |
||||
if err != nil { |
||||
logger.LogIf(context.Background(), err) |
||||
return rmode, r, ErrPastObjectLockRetainDate |
||||
} |
||||
|
||||
if retDate.Before(t) { |
||||
return rmode, r, ErrPastObjectLockRetainDate |
||||
} |
||||
|
||||
return rmode, RetentionDate{retDate}, nil |
||||
|
||||
} |
||||
|
||||
// GetObjectRetentionMeta constructs ObjectRetention from metadata
|
||||
func GetObjectRetentionMeta(meta map[string]string) ObjectRetention { |
||||
var mode Mode |
||||
var retainTill RetentionDate |
||||
|
||||
if modeStr, ok := meta[strings.ToLower(xhttp.AmzObjectLockMode)]; ok { |
||||
mode = parseMode(modeStr) |
||||
} |
||||
if tillStr, ok := meta[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)]; ok { |
||||
if t, e := time.Parse(time.RFC3339, tillStr); e == nil { |
||||
retainTill = RetentionDate{t.UTC()} |
||||
} |
||||
} |
||||
return ObjectRetention{Mode: mode, RetainUntilDate: retainTill} |
||||
} |
||||
|
||||
// GetObjectLegalHoldMeta constructs ObjectLegalHold from metadata
|
||||
func GetObjectLegalHoldMeta(meta map[string]string) ObjectLegalHold { |
||||
|
||||
holdStr, ok := meta[strings.ToLower(xhttp.AmzObjectLockLegalHold)] |
||||
if ok { |
||||
return ObjectLegalHold{Status: parseLegalHoldStatus(holdStr)} |
||||
} |
||||
return ObjectLegalHold{} |
||||
} |
||||
|
||||
// ParseObjectLockLegalHoldHeaders parses request headers to construct ObjectLegalHold
|
||||
func ParseObjectLockLegalHoldHeaders(h http.Header) (lhold ObjectLegalHold, err error) { |
||||
holdStatus, ok := h[xhttp.AmzObjectLockLegalHold] |
||||
if ok { |
||||
lh := parseLegalHoldStatus(strings.Join(holdStatus, "")) |
||||
if lh != ON && lh != OFF { |
||||
return lhold, ErrUnknownWORMModeDirective |
||||
} |
||||
lhold = ObjectLegalHold{Status: lh} |
||||
} |
||||
return lhold, nil |
||||
|
||||
} |
||||
|
||||
// ObjectLegalHold specified in
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectLegalHold.html
|
||||
type ObjectLegalHold struct { |
||||
XMLNS string `xml:"xmlns,attr,omitempty"` |
||||
XMLName xml.Name `xml:"LegalHold"` |
||||
Status LegalHoldStatus `xml:"Status,omitempty"` |
||||
} |
||||
|
||||
// ParseObjectLegalHold decodes the XML into ObjectLegalHold
|
||||
func ParseObjectLegalHold(reader io.Reader) (hold *ObjectLegalHold, err error) { |
||||
if err = xml.NewDecoder(reader).Decode(&hold); err != nil { |
||||
return |
||||
} |
||||
|
||||
if hold.Status != ON && hold.Status != OFF { |
||||
return nil, ErrMalformedXML |
||||
} |
||||
return |
||||
} |
||||
|
||||
// FilterObjectLockMetadata filters object lock metadata if s3:GetObjectRetention permission is denied or if isCopy flag set.
|
||||
func FilterObjectLockMetadata(metadata map[string]string, filterRetention, filterLegalHold bool) map[string]string { |
||||
// Copy on write
|
||||
dst := metadata |
||||
var copied bool |
||||
delKey := func(key string) { |
||||
if _, ok := metadata[key]; !ok { |
||||
return |
||||
} |
||||
if !copied { |
||||
dst = make(map[string]string, len(metadata)) |
||||
for k, v := range metadata { |
||||
dst[k] = v |
||||
} |
||||
copied = true |
||||
} |
||||
delete(dst, key) |
||||
} |
||||
legalHold := GetObjectLegalHoldMeta(metadata) |
||||
if legalHold.Status == "" || filterLegalHold { |
||||
delKey(xhttp.AmzObjectLockLegalHold) |
||||
} |
||||
|
||||
ret := GetObjectRetentionMeta(metadata) |
||||
|
||||
if ret.Mode == Invalid || filterRetention { |
||||
delKey(xhttp.AmzObjectLockMode) |
||||
delKey(xhttp.AmzObjectLockRetainUntilDate) |
||||
return dst |
||||
} |
||||
return dst |
||||
} |
@ -0,0 +1,567 @@ |
||||
/* |
||||
* MinIO Cloud Storage, (C) 2020 MinIO, Inc. |
||||
* |
||||
* 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 objectlock |
||||
|
||||
import ( |
||||
"encoding/xml" |
||||
"fmt" |
||||
"net/http" |
||||
"reflect" |
||||
"strings" |
||||
"testing" |
||||
"time" |
||||
|
||||
xhttp "github.com/minio/minio/cmd/http" |
||||
) |
||||
|
||||
func TestParseMode(t *testing.T) { |
||||
testCases := []struct { |
||||
value string |
||||
expectedMode Mode |
||||
}{ |
||||
{ |
||||
value: "governance", |
||||
expectedMode: Governance, |
||||
}, |
||||
{ |
||||
value: "complIAnce", |
||||
expectedMode: Compliance, |
||||
}, |
||||
{ |
||||
value: "gce", |
||||
expectedMode: Invalid, |
||||
}, |
||||
} |
||||
|
||||
for _, tc := range testCases { |
||||
if parseMode(tc.value) != tc.expectedMode { |
||||
t.Errorf("Expected Mode %s, got %s", tc.expectedMode, parseMode(tc.value)) |
||||
} |
||||
} |
||||
} |
||||
func TestParseLegalHoldStatus(t *testing.T) { |
||||
tests := []struct { |
||||
value string |
||||
expectedStatus LegalHoldStatus |
||||
}{ |
||||
{ |
||||
value: "ON", |
||||
expectedStatus: ON, |
||||
}, |
||||
{ |
||||
value: "Off", |
||||
expectedStatus: OFF, |
||||
}, |
||||
{ |
||||
value: "x", |
||||
expectedStatus: "", |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
actualStatus := parseLegalHoldStatus(tt.value) |
||||
if actualStatus != tt.expectedStatus { |
||||
t.Errorf("Expected legal hold status %s, got %s", tt.expectedStatus, actualStatus) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// TestUnmarshalDefaultRetention checks if default retention
|
||||
// marshaling and unmarshaling work as expected
|
||||
func TestUnmarshalDefaultRetention(t *testing.T) { |
||||
days := uint64(4) |
||||
years := uint64(1) |
||||
zerodays := uint64(0) |
||||
invalidDays := uint64(maximumRetentionDays + 1) |
||||
tests := []struct { |
||||
value DefaultRetention |
||||
expectedErr error |
||||
expectErr bool |
||||
}{ |
||||
{ |
||||
value: DefaultRetention{Mode: "retain"}, |
||||
expectedErr: fmt.Errorf("unknown retention mode retain"), |
||||
expectErr: true, |
||||
}, |
||||
{ |
||||
value: DefaultRetention{Mode: "GOVERNANCE"}, |
||||
expectedErr: fmt.Errorf("either Days or Years must be specified"), |
||||
expectErr: true, |
||||
}, |
||||
{ |
||||
value: DefaultRetention{Mode: "GOVERNANCE", Days: &days}, |
||||
expectedErr: nil, |
||||
expectErr: false, |
||||
}, |
||||
{ |
||||
value: DefaultRetention{Mode: "GOVERNANCE", Years: &years}, |
||||
expectedErr: nil, |
||||
expectErr: false, |
||||
}, |
||||
{ |
||||
value: DefaultRetention{Mode: "GOVERNANCE", Days: &days, Years: &years}, |
||||
expectedErr: fmt.Errorf("either Days or Years must be specified, not both"), |
||||
expectErr: true, |
||||
}, |
||||
{ |
||||
value: DefaultRetention{Mode: "GOVERNANCE", Days: &zerodays}, |
||||
expectedErr: fmt.Errorf("Default retention period must be a positive integer value for 'Days'"), |
||||
expectErr: true, |
||||
}, |
||||
{ |
||||
value: DefaultRetention{Mode: "GOVERNANCE", Days: &invalidDays}, |
||||
expectedErr: fmt.Errorf("Default retention period too large for 'Days' %d", invalidDays), |
||||
expectErr: true, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
d, err := xml.MarshalIndent(&tt.value, "", "\t") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
var dr DefaultRetention |
||||
err = xml.Unmarshal(d, &dr) |
||||
if tt.expectedErr == nil { |
||||
if err != nil { |
||||
t.Fatalf("error: expected = <nil>, got = %v", err) |
||||
} |
||||
} else if err == nil { |
||||
t.Fatalf("error: expected = %v, got = <nil>", tt.expectedErr) |
||||
} else if tt.expectedErr.Error() != err.Error() { |
||||
t.Fatalf("error: expected = %v, got = %v", tt.expectedErr, err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestParseObjectLockConfig(t *testing.T) { |
||||
tests := []struct { |
||||
value string |
||||
expectedErr error |
||||
expectErr bool |
||||
}{ |
||||
{ |
||||
value: "<ObjectLockConfiguration><ObjectLockEnabled>yes</ObjectLockEnabled></ObjectLockConfiguration>", |
||||
expectedErr: fmt.Errorf("only 'Enabled' value is allowd to ObjectLockEnabled element"), |
||||
expectErr: true, |
||||
}, |
||||
{ |
||||
value: "<ObjectLockConfiguration><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>COMPLIANCE</Mode><Days>0</Days></DefaultRetention></Rule></ObjectLockConfiguration>", |
||||
expectedErr: fmt.Errorf("Default retention period must be a positive integer value for 'Days'"), |
||||
expectErr: true, |
||||
}, |
||||
{ |
||||
value: "<ObjectLockConfiguration><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>COMPLIANCE</Mode><Days>30</Days></DefaultRetention></Rule></ObjectLockConfiguration>", |
||||
expectedErr: nil, |
||||
expectErr: false, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
_, err := ParseObjectLockConfig(strings.NewReader(tt.value)) |
||||
if tt.expectedErr == nil { |
||||
if err != nil { |
||||
t.Fatalf("error: expected = <nil>, got = %v", err) |
||||
} |
||||
} else if err == nil { |
||||
t.Fatalf("error: expected = %v, got = <nil>", tt.expectedErr) |
||||
} else if tt.expectedErr.Error() != err.Error() { |
||||
t.Fatalf("error: expected = %v, got = %v", tt.expectedErr, err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestParseObjectRetention(t *testing.T) { |
||||
tests := []struct { |
||||
value string |
||||
expectedErr error |
||||
expectErr bool |
||||
}{ |
||||
{ |
||||
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Retention xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Mode>string</Mode><RetainUntilDate>2020-01-02T15:04:05Z</RetainUntilDate></Retention>", |
||||
expectedErr: ErrUnknownWORMModeDirective, |
||||
expectErr: true, |
||||
}, |
||||
{ |
||||
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Retention xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Mode>COMPLIANCE</Mode><RetainUntilDate>2017-01-02T15:04:05Z</RetainUntilDate></Retention>", |
||||
expectedErr: ErrPastObjectLockRetainDate, |
||||
expectErr: true, |
||||
}, |
||||
{ |
||||
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Retention xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Mode>GOVERNANCE</Mode><RetainUntilDate>2057-01-02T15:04:05Z</RetainUntilDate></Retention>", |
||||
expectedErr: nil, |
||||
expectErr: false, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
_, err := ParseObjectRetention(strings.NewReader(tt.value)) |
||||
if tt.expectedErr == nil { |
||||
if err != nil { |
||||
t.Fatalf("error: expected = <nil>, got = %v", err) |
||||
} |
||||
} else if err == nil { |
||||
t.Fatalf("error: expected = %v, got = <nil>", tt.expectedErr) |
||||
} else if tt.expectedErr.Error() != err.Error() { |
||||
t.Fatalf("error: expected = %v, got = %v", tt.expectedErr, err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestIsObjectLockRequested(t *testing.T) { |
||||
tests := []struct { |
||||
header http.Header |
||||
expectedVal bool |
||||
}{ |
||||
{ |
||||
header: http.Header{ |
||||
"Authorization": []string{"AWS4-HMAC-SHA256 <cred_string>"}, |
||||
"X-Amz-Content-Sha256": []string{""}, |
||||
"Content-Encoding": []string{""}, |
||||
}, |
||||
expectedVal: false, |
||||
}, |
||||
{ |
||||
header: http.Header{ |
||||
xhttp.AmzObjectLockLegalHold: []string{""}, |
||||
}, |
||||
expectedVal: true, |
||||
}, |
||||
{ |
||||
header: http.Header{ |
||||
xhttp.AmzObjectLockRetainUntilDate: []string{""}, |
||||
xhttp.AmzObjectLockMode: []string{""}, |
||||
}, |
||||
expectedVal: true, |
||||
}, |
||||
{ |
||||
header: http.Header{ |
||||
xhttp.AmzObjectLockBypassGovernance: []string{""}, |
||||
}, |
||||
expectedVal: false, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
actualVal := IsObjectLockRequested(tt.header) |
||||
if actualVal != tt.expectedVal { |
||||
t.Fatalf("error: expected %v, actual %v", tt.expectedVal, actualVal) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestIsObjectLockGovernanceBypassSet(t *testing.T) { |
||||
tests := []struct { |
||||
header http.Header |
||||
expectedVal bool |
||||
}{ |
||||
{ |
||||
header: http.Header{ |
||||
"Authorization": []string{"AWS4-HMAC-SHA256 <cred_string>"}, |
||||
"X-Amz-Content-Sha256": []string{""}, |
||||
"Content-Encoding": []string{""}, |
||||
}, |
||||
expectedVal: false, |
||||
}, |
||||
{ |
||||
header: http.Header{ |
||||
xhttp.AmzObjectLockLegalHold: []string{""}, |
||||
}, |
||||
expectedVal: false, |
||||
}, |
||||
{ |
||||
header: http.Header{ |
||||
xhttp.AmzObjectLockRetainUntilDate: []string{""}, |
||||
xhttp.AmzObjectLockMode: []string{""}, |
||||
}, |
||||
expectedVal: false, |
||||
}, |
||||
{ |
||||
header: http.Header{ |
||||
xhttp.AmzObjectLockBypassGovernance: []string{""}, |
||||
}, |
||||
expectedVal: false, |
||||
}, |
||||
{ |
||||
header: http.Header{ |
||||
xhttp.AmzObjectLockBypassGovernance: []string{"true"}, |
||||
}, |
||||
expectedVal: true, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
actualVal := IsObjectLockGovernanceBypassSet(tt.header) |
||||
if actualVal != tt.expectedVal { |
||||
t.Fatalf("error: expected %v, actual %v", tt.expectedVal, actualVal) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestParseObjectLockRetentionHeaders(t *testing.T) { |
||||
tests := []struct { |
||||
header http.Header |
||||
expectedErr error |
||||
}{ |
||||
{ |
||||
header: http.Header{ |
||||
"Authorization": []string{"AWS4-HMAC-SHA256 <cred_string>"}, |
||||
"X-Amz-Content-Sha256": []string{""}, |
||||
"Content-Encoding": []string{""}, |
||||
}, |
||||
expectedErr: ErrObjectLockInvalidHeaders, |
||||
}, |
||||
{ |
||||
header: http.Header{ |
||||
xhttp.AmzObjectLockMode: []string{"lock"}, |
||||
xhttp.AmzObjectLockRetainUntilDate: []string{"2017-01-02"}, |
||||
}, |
||||
expectedErr: ErrUnknownWORMModeDirective, |
||||
}, |
||||
{ |
||||
header: http.Header{ |
||||
xhttp.AmzObjectLockMode: []string{"governance"}, |
||||
}, |
||||
expectedErr: ErrObjectLockInvalidHeaders, |
||||
}, |
||||
{ |
||||
header: http.Header{ |
||||
xhttp.AmzObjectLockRetainUntilDate: []string{"2017-01-02"}, |
||||
xhttp.AmzObjectLockMode: []string{"governance"}, |
||||
}, |
||||
expectedErr: ErrInvalidRetentionDate, |
||||
}, |
||||
{ |
||||
header: http.Header{ |
||||
xhttp.AmzObjectLockRetainUntilDate: []string{"2017-01-02T15:04:05Z"}, |
||||
xhttp.AmzObjectLockMode: []string{"governance"}, |
||||
}, |
||||
expectedErr: ErrPastObjectLockRetainDate, |
||||
}, |
||||
{ |
||||
header: http.Header{ |
||||
xhttp.AmzObjectLockMode: []string{"governance"}, |
||||
xhttp.AmzObjectLockRetainUntilDate: []string{"2017-01-02T15:04:05Z"}, |
||||
}, |
||||
expectedErr: ErrPastObjectLockRetainDate, |
||||
}, |
||||
{ |
||||
header: http.Header{ |
||||
xhttp.AmzObjectLockMode: []string{"governance"}, |
||||
xhttp.AmzObjectLockRetainUntilDate: []string{"2087-01-02T15:04:05Z"}, |
||||
}, |
||||
expectedErr: nil, |
||||
}, |
||||
} |
||||
|
||||
for i, tt := range tests { |
||||
_, _, err := ParseObjectLockRetentionHeaders(tt.header) |
||||
if tt.expectedErr == nil { |
||||
if err != nil { |
||||
t.Fatalf("Case %d error: expected = <nil>, got = %v", i, err) |
||||
} |
||||
} else if err == nil { |
||||
t.Fatalf("Case %d error: expected = %v, got = <nil>", i, tt.expectedErr) |
||||
} else if tt.expectedErr.Error() != err.Error() { |
||||
t.Fatalf("Case %d error: expected = %v, got = %v", i, tt.expectedErr, err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestGetObjectRetentionMeta(t *testing.T) { |
||||
tests := []struct { |
||||
metadata map[string]string |
||||
expected ObjectRetention |
||||
}{ |
||||
{ |
||||
metadata: map[string]string{ |
||||
"Authorization": "AWS4-HMAC-SHA256 <cred_string>", |
||||
"X-Amz-Content-Sha256": "", |
||||
"Content-Encoding": "", |
||||
}, |
||||
expected: ObjectRetention{}, |
||||
}, |
||||
{ |
||||
metadata: map[string]string{ |
||||
"x-amz-object-lock-mode": "governance", |
||||
}, |
||||
expected: ObjectRetention{Mode: Governance}, |
||||
}, |
||||
{ |
||||
metadata: map[string]string{ |
||||
"x-amz-object-lock-retain-until-date": "2020-02-01", |
||||
}, |
||||
expected: ObjectRetention{RetainUntilDate: RetentionDate{time.Date(2020, 2, 1, 12, 0, 0, 0, time.UTC)}}, |
||||
}, |
||||
} |
||||
|
||||
for i, tt := range tests { |
||||
o := GetObjectRetentionMeta(tt.metadata) |
||||
if o.Mode != tt.expected.Mode { |
||||
t.Fatalf("Case %d expected %v, got %v", i, tt.expected.Mode, o.Mode) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestGetObjectLegalHoldMeta(t *testing.T) { |
||||
tests := []struct { |
||||
metadata map[string]string |
||||
expected ObjectLegalHold |
||||
}{ |
||||
{ |
||||
metadata: map[string]string{ |
||||
"x-amz-object-lock-mode": "governance", |
||||
}, |
||||
expected: ObjectLegalHold{}, |
||||
}, |
||||
{ |
||||
metadata: map[string]string{ |
||||
"x-amz-object-lock-legal-hold": "on", |
||||
}, |
||||
expected: ObjectLegalHold{Status: ON}, |
||||
}, |
||||
{ |
||||
metadata: map[string]string{ |
||||
"x-amz-object-lock-legal-hold": "off", |
||||
}, |
||||
expected: ObjectLegalHold{Status: OFF}, |
||||
}, |
||||
{ |
||||
metadata: map[string]string{ |
||||
"x-amz-object-lock-legal-hold": "X", |
||||
}, |
||||
expected: ObjectLegalHold{Status: ""}, |
||||
}, |
||||
} |
||||
|
||||
for i, tt := range tests { |
||||
o := GetObjectLegalHoldMeta(tt.metadata) |
||||
if o.Status != tt.expected.Status { |
||||
t.Fatalf("Case %d expected %v, got %v", i, tt.expected.Status, o.Status) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestParseObjectLegalHold(t *testing.T) { |
||||
tests := []struct { |
||||
value string |
||||
expectedErr error |
||||
expectErr bool |
||||
}{ |
||||
{ |
||||
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><LegalHold xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Status>string</Status></LegalHold>", |
||||
expectedErr: ErrMalformedXML, |
||||
expectErr: true, |
||||
}, |
||||
{ |
||||
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><LegalHold xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Status>ON</Status></LegalHold>", |
||||
expectedErr: nil, |
||||
expectErr: false, |
||||
}, |
||||
{ |
||||
value: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><LegalHold xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Status>On</Status></LegalHold>", |
||||
expectedErr: ErrMalformedXML, |
||||
expectErr: true, |
||||
}, |
||||
} |
||||
for i, tt := range tests { |
||||
_, err := ParseObjectLegalHold(strings.NewReader(tt.value)) |
||||
if tt.expectedErr == nil { |
||||
if err != nil { |
||||
t.Fatalf("Case %d error: expected = <nil>, got = %v", i, err) |
||||
} |
||||
} else if err == nil { |
||||
t.Fatalf("Case %d error: expected = %v, got = <nil>", i, tt.expectedErr) |
||||
} else if tt.expectedErr.Error() != err.Error() { |
||||
t.Fatalf("Case %d error: expected = %v, got = %v", i, tt.expectedErr, err) |
||||
} |
||||
} |
||||
} |
||||
func TestFilterObjectLockMetadata(t *testing.T) { |
||||
tests := []struct { |
||||
metadata map[string]string |
||||
filterRetention bool |
||||
filterLegalHold bool |
||||
expected map[string]string |
||||
}{ |
||||
{ |
||||
metadata: map[string]string{ |
||||
"Authorization": "AWS4-HMAC-SHA256 <cred_string>", |
||||
"X-Amz-Content-Sha256": "", |
||||
"Content-Encoding": "", |
||||
}, |
||||
expected: map[string]string{ |
||||
"Authorization": "AWS4-HMAC-SHA256 <cred_string>", |
||||
"X-Amz-Content-Sha256": "", |
||||
"Content-Encoding": "", |
||||
}, |
||||
}, |
||||
{ |
||||
metadata: map[string]string{ |
||||
"x-amz-object-lock-mode": "governance", |
||||
}, |
||||
expected: map[string]string{ |
||||
"x-amz-object-lock-mode": "governance", |
||||
}, |
||||
filterRetention: false, |
||||
}, |
||||
{ |
||||
metadata: map[string]string{ |
||||
"x-amz-object-lock-mode": "governance", |
||||
"x-amz-object-lock-retain-until-date": "2020-02-01", |
||||
}, |
||||
expected: map[string]string{}, |
||||
filterRetention: true, |
||||
}, |
||||
{ |
||||
metadata: map[string]string{ |
||||
"x-amz-object-lock-legal-hold": "off", |
||||
}, |
||||
expected: map[string]string{}, |
||||
filterLegalHold: true, |
||||
}, |
||||
{ |
||||
metadata: map[string]string{ |
||||
"x-amz-object-lock-legal-hold": "on", |
||||
}, |
||||
expected: map[string]string{"x-amz-object-lock-legal-hold": "on"}, |
||||
filterLegalHold: false, |
||||
}, |
||||
{ |
||||
metadata: map[string]string{ |
||||
"x-amz-object-lock-legal-hold": "on", |
||||
"x-amz-object-lock-mode": "governance", |
||||
"x-amz-object-lock-retain-until-date": "2020-02-01", |
||||
}, |
||||
expected: map[string]string{}, |
||||
filterRetention: true, |
||||
filterLegalHold: true, |
||||
}, |
||||
{ |
||||
metadata: map[string]string{ |
||||
"x-amz-object-lock-legal-hold": "on", |
||||
"x-amz-object-lock-mode": "governance", |
||||
"x-amz-object-lock-retain-until-date": "2020-02-01", |
||||
}, |
||||
expected: map[string]string{"x-amz-object-lock-legal-hold": "on", |
||||
"x-amz-object-lock-mode": "governance", |
||||
"x-amz-object-lock-retain-until-date": "2020-02-01"}, |
||||
}, |
||||
} |
||||
|
||||
for i, tt := range tests { |
||||
o := FilterObjectLockMetadata(tt.metadata, tt.filterRetention, tt.filterLegalHold) |
||||
if !reflect.DeepEqual(o, tt.metadata) { |
||||
t.Fatalf("Case %d expected %v, got %v", i, tt.metadata, o) |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue