mirror of
https://github.com/pgsty/minio.git
synced 2026-07-19 20:20:25 +03:00
Rename pkg/{tagging,lifecycle} to pkg/bucket sub-directory (#8892)
Rename to allow for more such features to come in a more proper hierarchical manner.
This commit is contained in:
committed by
kannappanr
parent
4cb6ebcfa2
commit
0cbebf0f57
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 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 lifecycle
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// And - a tag to combine a prefix and multiple tags for lifecycle configuration rule.
|
||||
type And struct {
|
||||
XMLName xml.Name `xml:"And"`
|
||||
Prefix string `xml:"Prefix,omitempty"`
|
||||
Tags []Tag `xml:"Tag,omitempty"`
|
||||
}
|
||||
|
||||
var errAndUnsupported = errors.New("Specifying <And></And> tag is not supported")
|
||||
|
||||
// UnmarshalXML is extended to indicate lack of support for And xml
|
||||
// tag in object lifecycle configuration
|
||||
func (a And) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
return errAndUnsupported
|
||||
}
|
||||
|
||||
// MarshalXML is extended to leave out <And></And> tags
|
||||
func (a And) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 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 lifecycle
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
errLifecycleInvalidDate = errors.New("Date must be provided in ISO 8601 format")
|
||||
errLifecycleInvalidDays = errors.New("Days must be positive integer when used with Expiration")
|
||||
errLifecycleInvalidExpiration = errors.New("At least one of Days or Date should be present inside Expiration")
|
||||
errLifecycleDateNotMidnight = errors.New(" 'Date' must be at midnight GMT")
|
||||
)
|
||||
|
||||
// ExpirationDays is a type alias to unmarshal Days in Expiration
|
||||
type ExpirationDays int
|
||||
|
||||
// UnmarshalXML parses number of days from Expiration and validates if
|
||||
// greater than zero
|
||||
func (eDays *ExpirationDays) UnmarshalXML(d *xml.Decoder, startElement xml.StartElement) error {
|
||||
var numDays int
|
||||
err := d.DecodeElement(&numDays, &startElement)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if numDays <= 0 {
|
||||
return errLifecycleInvalidDays
|
||||
}
|
||||
*eDays = ExpirationDays(numDays)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalXML encodes number of days to expire if it is non-zero and
|
||||
// encodes empty string otherwise
|
||||
func (eDays *ExpirationDays) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
|
||||
if *eDays == ExpirationDays(0) {
|
||||
return nil
|
||||
}
|
||||
return e.EncodeElement(int(*eDays), startElement)
|
||||
}
|
||||
|
||||
// ExpirationDate is a embedded type containing time.Time to unmarshal
|
||||
// Date in Expiration
|
||||
type ExpirationDate struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
// UnmarshalXML parses date from Expiration and validates date format
|
||||
func (eDate *ExpirationDate) 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.
|
||||
expDate, err := time.Parse(time.RFC3339, dateStr)
|
||||
if err != nil {
|
||||
return errLifecycleInvalidDate
|
||||
}
|
||||
// Allow only date timestamp specifying midnight GMT
|
||||
hr, min, sec := expDate.Clock()
|
||||
nsec := expDate.Nanosecond()
|
||||
loc := expDate.Location()
|
||||
if !(hr == 0 && min == 0 && sec == 0 && nsec == 0 && loc.String() == time.UTC.String()) {
|
||||
return errLifecycleDateNotMidnight
|
||||
}
|
||||
|
||||
*eDate = ExpirationDate{expDate}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalXML encodes expiration date if it is non-zero and encodes
|
||||
// empty string otherwise
|
||||
func (eDate *ExpirationDate) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
|
||||
if *eDate == (ExpirationDate{time.Time{}}) {
|
||||
return nil
|
||||
}
|
||||
return e.EncodeElement(eDate.Format(time.RFC3339), startElement)
|
||||
}
|
||||
|
||||
// Expiration - expiration actions for a rule in lifecycle configuration.
|
||||
type Expiration struct {
|
||||
XMLName xml.Name `xml:"Expiration"`
|
||||
Days ExpirationDays `xml:"Days,omitempty"`
|
||||
Date ExpirationDate `xml:"Date,omitempty"`
|
||||
}
|
||||
|
||||
// Validate - validates the "Expiration" element
|
||||
func (e Expiration) Validate() error {
|
||||
// Neither expiration days or date is specified
|
||||
if e.IsDaysNull() && e.IsDateNull() {
|
||||
return errLifecycleInvalidExpiration
|
||||
}
|
||||
|
||||
// Both expiration days and date are specified
|
||||
if !e.IsDaysNull() && !e.IsDateNull() {
|
||||
return errLifecycleInvalidExpiration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsDaysNull returns true if days field is null
|
||||
func (e Expiration) IsDaysNull() bool {
|
||||
return e.Days == ExpirationDays(0)
|
||||
|
||||
}
|
||||
|
||||
// IsDateNull returns true if date field is null
|
||||
func (e Expiration) IsDateNull() bool {
|
||||
return e.Date == ExpirationDate{time.Time{}}
|
||||
}
|
||||
|
||||
// IsNull returns true if both date and days fields are null
|
||||
func (e Expiration) IsNull() bool {
|
||||
return e.IsDaysNull() && e.IsDateNull()
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 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 lifecycle
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// appropriate errors on validation
|
||||
func TestInvalidExpiration(t *testing.T) {
|
||||
testCases := []struct {
|
||||
inputXML string
|
||||
expectedErr error
|
||||
}{
|
||||
{ // Expiration with zero days
|
||||
inputXML: ` <Expiration>
|
||||
<Days>0</Days>
|
||||
</Expiration>`,
|
||||
expectedErr: errLifecycleInvalidDays,
|
||||
},
|
||||
{ // Expiration with invalid date
|
||||
inputXML: ` <Expiration>
|
||||
<Date>invalid date</Date>
|
||||
</Expiration>`,
|
||||
expectedErr: errLifecycleInvalidDate,
|
||||
},
|
||||
{ // Expiration with both number of days nor a date
|
||||
inputXML: `<Expiration>
|
||||
<Date>2019-04-20T00:01:00Z</Date>
|
||||
</Expiration>`,
|
||||
expectedErr: errLifecycleDateNotMidnight,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) {
|
||||
var expiration Expiration
|
||||
err := xml.Unmarshal([]byte(tc.inputXML), &expiration)
|
||||
if err != tc.expectedErr {
|
||||
t.Fatalf("%d: Expected %v but got %v", i+1, tc.expectedErr, err)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
validationTestCases := []struct {
|
||||
inputXML string
|
||||
expectedErr error
|
||||
}{
|
||||
{ // Expiration with a valid ISO 8601 date
|
||||
inputXML: `<Expiration>
|
||||
<Date>2019-04-20T00:00:00Z</Date>
|
||||
</Expiration>`,
|
||||
expectedErr: nil,
|
||||
},
|
||||
{ // Expiration with a valid number of days
|
||||
inputXML: `<Expiration>
|
||||
<Days>3</Days>
|
||||
</Expiration>`,
|
||||
expectedErr: nil,
|
||||
},
|
||||
{ // Expiration with neither number of days nor a date
|
||||
inputXML: `<Expiration>
|
||||
</Expiration>`,
|
||||
expectedErr: errLifecycleInvalidExpiration,
|
||||
},
|
||||
{ // Expiration with both number of days nor a date
|
||||
inputXML: `<Expiration>
|
||||
<Days>3</Days>
|
||||
<Date>2019-04-20T00:00:00Z</Date>
|
||||
</Expiration>`,
|
||||
expectedErr: errLifecycleInvalidExpiration,
|
||||
},
|
||||
}
|
||||
for i, tc := range validationTestCases {
|
||||
t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) {
|
||||
var expiration Expiration
|
||||
err := xml.Unmarshal([]byte(tc.inputXML), &expiration)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: %v", i+1, err)
|
||||
}
|
||||
|
||||
err = expiration.Validate()
|
||||
if err != tc.expectedErr {
|
||||
t.Fatalf("%d: %v", i+1, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 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 lifecycle
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// Filter - a filter for a lifecycle configuration Rule.
|
||||
type Filter struct {
|
||||
XMLName xml.Name `xml:"Filter"`
|
||||
And And `xml:"And,omitempty"`
|
||||
Prefix string `xml:"Prefix"`
|
||||
Tag Tag `xml:"Tag,omitempty"`
|
||||
}
|
||||
|
||||
// Validate - validates the filter element
|
||||
func (f Filter) Validate() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 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 lifecycle
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestUnsupportedFilters checks if parsing Filter xml with
|
||||
// unsupported elements returns appropriate errors
|
||||
func TestUnsupportedFilters(t *testing.T) {
|
||||
testCases := []struct {
|
||||
inputXML string
|
||||
expectedErr error
|
||||
}{
|
||||
{ // Filter with And tags
|
||||
inputXML: ` <Filter>
|
||||
<And>
|
||||
<Prefix></Prefix>
|
||||
</And>
|
||||
</Filter>`,
|
||||
expectedErr: errAndUnsupported,
|
||||
},
|
||||
{ // Filter with Tag tags
|
||||
inputXML: ` <Filter>
|
||||
<Tag></Tag>
|
||||
</Filter>`,
|
||||
expectedErr: errTagUnsupported,
|
||||
},
|
||||
}
|
||||
for i, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) {
|
||||
var filter Filter
|
||||
err := xml.Unmarshal([]byte(tc.inputXML), &filter)
|
||||
if err != tc.expectedErr {
|
||||
t.Fatalf("%d: Expected %v but got %v", i+1, tc.expectedErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 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 lifecycle
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
errLifecycleTooManyRules = errors.New("Lifecycle configuration allows a maximum of 1000 rules")
|
||||
errLifecycleNoRule = errors.New("Lifecycle configuration should have at least one rule")
|
||||
errLifecycleOverlappingPrefix = errors.New("Lifecycle configuration has rules with overlapping prefix")
|
||||
)
|
||||
|
||||
// Action represents a delete action or other transition
|
||||
// actions that will be implemented later.
|
||||
type Action int
|
||||
|
||||
const (
|
||||
// NoneAction means no action required after evaluting lifecycle rules
|
||||
NoneAction Action = iota
|
||||
// DeleteAction means the object needs to be removed after evaluting lifecycle rules
|
||||
DeleteAction
|
||||
)
|
||||
|
||||
// Lifecycle - Configuration for bucket lifecycle.
|
||||
type Lifecycle struct {
|
||||
XMLName xml.Name `xml:"LifecycleConfiguration"`
|
||||
Rules []Rule `xml:"Rule"`
|
||||
}
|
||||
|
||||
// IsEmpty - returns whether policy is empty or not.
|
||||
func (lc Lifecycle) IsEmpty() bool {
|
||||
return len(lc.Rules) == 0
|
||||
}
|
||||
|
||||
// ParseLifecycleConfig - parses data in given reader to Lifecycle.
|
||||
func ParseLifecycleConfig(reader io.Reader) (*Lifecycle, error) {
|
||||
var lc Lifecycle
|
||||
if err := xml.NewDecoder(reader).Decode(&lc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := lc.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &lc, nil
|
||||
}
|
||||
|
||||
// Validate - validates the lifecycle configuration
|
||||
func (lc Lifecycle) Validate() error {
|
||||
// Lifecycle config can't have more than 1000 rules
|
||||
if len(lc.Rules) > 1000 {
|
||||
return errLifecycleTooManyRules
|
||||
}
|
||||
// Lifecycle config should have at least one rule
|
||||
if len(lc.Rules) == 0 {
|
||||
return errLifecycleNoRule
|
||||
}
|
||||
// Validate all the rules in the lifecycle config
|
||||
for _, r := range lc.Rules {
|
||||
if err := r.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Compare every rule's prefix with every other rule's prefix
|
||||
for i := range lc.Rules {
|
||||
if i == len(lc.Rules)-1 {
|
||||
break
|
||||
}
|
||||
// N B Empty prefixes overlap with all prefixes
|
||||
otherRules := lc.Rules[i+1:]
|
||||
for _, otherRule := range otherRules {
|
||||
if strings.HasPrefix(lc.Rules[i].Filter.Prefix, otherRule.Filter.Prefix) ||
|
||||
strings.HasPrefix(otherRule.Filter.Prefix, lc.Rules[i].Filter.Prefix) {
|
||||
return errLifecycleOverlappingPrefix
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilterRuleActions returns the expiration and transition from the object name
|
||||
// after evaluating all rules.
|
||||
func (lc Lifecycle) FilterRuleActions(objName string) (Expiration, Transition) {
|
||||
for _, rule := range lc.Rules {
|
||||
if strings.ToLower(rule.Status) != "enabled" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(objName, rule.Filter.Prefix) {
|
||||
return rule.Expiration, Transition{}
|
||||
}
|
||||
}
|
||||
return Expiration{}, Transition{}
|
||||
}
|
||||
|
||||
// ComputeAction returns the action to perform by evaluating all lifecycle rules
|
||||
// against the object name and its modification time.
|
||||
func (lc Lifecycle) ComputeAction(objName string, modTime time.Time) Action {
|
||||
var action = NoneAction
|
||||
exp, _ := lc.FilterRuleActions(objName)
|
||||
if !exp.IsDateNull() {
|
||||
if time.Now().After(exp.Date.Time) {
|
||||
action = DeleteAction
|
||||
}
|
||||
}
|
||||
if !exp.IsDaysNull() {
|
||||
if time.Now().After(modTime.Add(time.Duration(exp.Days) * 24 * time.Hour)) {
|
||||
action = DeleteAction
|
||||
}
|
||||
}
|
||||
return action
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 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 lifecycle
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestParseLifecycleConfig(t *testing.T) {
|
||||
// Test for lifecycle config with more than 1000 rules
|
||||
var manyRules []Rule
|
||||
rule := Rule{
|
||||
Status: "Enabled",
|
||||
Expiration: Expiration{Days: ExpirationDays(3)},
|
||||
}
|
||||
for i := 0; i < 1001; i++ {
|
||||
manyRules = append(manyRules, rule)
|
||||
}
|
||||
|
||||
manyRuleLcConfig, err := xml.Marshal(Lifecycle{Rules: manyRules})
|
||||
if err != nil {
|
||||
t.Fatal("Failed to marshal lifecycle config with more than 1000 rules")
|
||||
}
|
||||
|
||||
// Test for lifecycle config with rules containing overlapping prefixes
|
||||
rule1 := Rule{
|
||||
Status: "Enabled",
|
||||
Expiration: Expiration{Days: ExpirationDays(3)},
|
||||
Filter: Filter{
|
||||
Prefix: "/a/b",
|
||||
},
|
||||
}
|
||||
rule2 := Rule{
|
||||
Status: "Enabled",
|
||||
Expiration: Expiration{Days: ExpirationDays(3)},
|
||||
Filter: Filter{
|
||||
Prefix: "/a/b/c",
|
||||
},
|
||||
}
|
||||
overlappingRules := []Rule{rule1, rule2}
|
||||
overlappingLcConfig, err := xml.Marshal(Lifecycle{Rules: overlappingRules})
|
||||
if err != nil {
|
||||
t.Fatal("Failed to marshal lifecycle config with rules having overlapping prefix")
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
inputConfig string
|
||||
expectedErr error
|
||||
}{
|
||||
{ // Valid lifecycle config
|
||||
inputConfig: `<LifecycleConfiguration>
|
||||
<Rule>
|
||||
<Filter>
|
||||
<Prefix>prefix</Prefix>
|
||||
</Filter>
|
||||
<Status>Enabled</Status>
|
||||
<Expiration><Days>3</Days></Expiration>
|
||||
</Rule>
|
||||
<Rule>
|
||||
<Filter>
|
||||
<Prefix>another-prefix</Prefix>
|
||||
</Filter>
|
||||
<Status>Enabled</Status>
|
||||
<Expiration><Days>3</Days></Expiration>
|
||||
</Rule>
|
||||
</LifecycleConfiguration>`,
|
||||
expectedErr: nil,
|
||||
},
|
||||
{ // lifecycle config with no rules
|
||||
inputConfig: `<LifecycleConfiguration>
|
||||
</LifecycleConfiguration>`,
|
||||
expectedErr: errLifecycleNoRule,
|
||||
},
|
||||
{ // lifecycle config with more than 1000 rules
|
||||
inputConfig: string(manyRuleLcConfig),
|
||||
expectedErr: errLifecycleTooManyRules,
|
||||
},
|
||||
{ // lifecycle config with rules having overlapping prefix
|
||||
inputConfig: string(overlappingLcConfig),
|
||||
expectedErr: errLifecycleOverlappingPrefix,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) {
|
||||
var err error
|
||||
if _, err = ParseLifecycleConfig(bytes.NewReader([]byte(tc.inputConfig))); err != tc.expectedErr {
|
||||
t.Fatalf("%d: Expected %v but got %v", i+1, tc.expectedErr, err)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// TestMarshalLifecycleConfig checks if lifecycleconfig xml
|
||||
// marshaling/unmarshaling can handle output from each other
|
||||
func TestMarshalLifecycleConfig(t *testing.T) {
|
||||
// Time at midnight UTC
|
||||
midnightTS := ExpirationDate{time.Date(2019, time.April, 20, 0, 0, 0, 0, time.UTC)}
|
||||
lc := Lifecycle{
|
||||
Rules: []Rule{
|
||||
{
|
||||
Status: "Enabled",
|
||||
Filter: Filter{Prefix: "prefix-1"},
|
||||
Expiration: Expiration{Days: ExpirationDays(3)},
|
||||
},
|
||||
{
|
||||
Status: "Enabled",
|
||||
Filter: Filter{Prefix: "prefix-1"},
|
||||
Expiration: Expiration{Date: ExpirationDate(midnightTS)},
|
||||
},
|
||||
},
|
||||
}
|
||||
b, err := xml.MarshalIndent(&lc, "", "\t")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var lc1 Lifecycle
|
||||
err = xml.Unmarshal(b, &lc1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ruleSet := make(map[string]struct{})
|
||||
for _, rule := range lc.Rules {
|
||||
ruleBytes, err := xml.Marshal(rule)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ruleSet[string(ruleBytes)] = struct{}{}
|
||||
}
|
||||
for _, rule := range lc1.Rules {
|
||||
ruleBytes, err := xml.Marshal(rule)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := ruleSet[string(ruleBytes)]; !ok {
|
||||
t.Fatalf("Expected %v to be equal to %v, %v missing", lc, lc1, rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeActions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
inputConfig string
|
||||
objectName string
|
||||
objectModTime time.Time
|
||||
expectedAction Action
|
||||
}{
|
||||
// Empty object name (unexpected case) should always return NoneAction
|
||||
{
|
||||
inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>prefix</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`,
|
||||
expectedAction: NoneAction,
|
||||
},
|
||||
// Disabled should always return NoneAction
|
||||
{
|
||||
inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Disabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`,
|
||||
objectName: "foodir/fooobject",
|
||||
objectModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), // Created 10 days ago
|
||||
expectedAction: NoneAction,
|
||||
},
|
||||
// Prefix not matched
|
||||
{
|
||||
inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`,
|
||||
objectName: "foxdir/fooobject",
|
||||
objectModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), // Created 10 days ago
|
||||
expectedAction: NoneAction,
|
||||
},
|
||||
// Too early to remove (test Days)
|
||||
{
|
||||
inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`,
|
||||
objectName: "foxdir/fooobject",
|
||||
objectModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), // Created 10 days ago
|
||||
expectedAction: NoneAction,
|
||||
},
|
||||
// Should remove (test Days)
|
||||
{
|
||||
inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`,
|
||||
objectName: "foodir/fooobject",
|
||||
objectModTime: time.Now().UTC().Add(-6 * 24 * time.Hour), // Created 6 days ago
|
||||
expectedAction: DeleteAction,
|
||||
},
|
||||
// Too early to remove (test Date)
|
||||
{
|
||||
inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`,
|
||||
objectName: "foodir/fooobject",
|
||||
objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago
|
||||
expectedAction: NoneAction,
|
||||
},
|
||||
// Should remove (test Days)
|
||||
{
|
||||
inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`,
|
||||
objectName: "foodir/fooobject",
|
||||
objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago
|
||||
expectedAction: DeleteAction,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) {
|
||||
lc, err := ParseLifecycleConfig(bytes.NewReader([]byte(tc.inputConfig)))
|
||||
if err != nil {
|
||||
t.Fatalf("%d: Got unexpected error: %v", i+1, err)
|
||||
}
|
||||
if resultAction := lc.ComputeAction(tc.objectName, tc.objectModTime); resultAction != tc.expectedAction {
|
||||
t.Fatalf("%d: Expected action: `%v`, got: `%v`", i+1, tc.expectedAction, resultAction)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 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 lifecycle
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// NoncurrentVersionExpiration - an action for lifecycle configuration rule.
|
||||
type NoncurrentVersionExpiration struct {
|
||||
XMLName xml.Name `xml:"NoncurrentVersionExpiration"`
|
||||
NoncurrentDays int `xml:"NoncurrentDays,omitempty"`
|
||||
}
|
||||
|
||||
// NoncurrentVersionTransition - an action for lifecycle configuration rule.
|
||||
type NoncurrentVersionTransition struct {
|
||||
NoncurrentDays int `xml:"NoncurrentDays"`
|
||||
StorageClass string `xml:"StorageClass"`
|
||||
}
|
||||
|
||||
var (
|
||||
errNoncurrentVersionExpirationUnsupported = errors.New("Specifying <NoncurrentVersionExpiration></NoncurrentVersionExpiration> is not supported")
|
||||
errNoncurrentVersionTransitionUnsupported = errors.New("Specifying <NoncurrentVersionTransition></NoncurrentVersionTransition> is not supported")
|
||||
)
|
||||
|
||||
// UnmarshalXML is extended to indicate lack of support for
|
||||
// NoncurrentVersionExpiration xml tag in object lifecycle
|
||||
// configuration
|
||||
func (n NoncurrentVersionExpiration) UnmarshalXML(d *xml.Decoder, startElement xml.StartElement) error {
|
||||
return errNoncurrentVersionExpirationUnsupported
|
||||
}
|
||||
|
||||
// UnmarshalXML is extended to indicate lack of support for
|
||||
// NoncurrentVersionTransition xml tag in object lifecycle
|
||||
// configuration
|
||||
func (n NoncurrentVersionTransition) UnmarshalXML(d *xml.Decoder, startElement xml.StartElement) error {
|
||||
return errNoncurrentVersionTransitionUnsupported
|
||||
}
|
||||
|
||||
// MarshalXML is extended to leave out
|
||||
// <NoncurrentVersionTransition></NoncurrentVersionTransition> tags
|
||||
func (n NoncurrentVersionTransition) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalXML is extended to leave out
|
||||
// <NoncurrentVersionExpiration></NoncurrentVersionExpiration> tags
|
||||
func (n NoncurrentVersionExpiration) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 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 lifecycle
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Rule - a rule for lifecycle configuration.
|
||||
type Rule struct {
|
||||
XMLName xml.Name `xml:"Rule"`
|
||||
ID string `xml:"ID,omitempty"`
|
||||
Status string `xml:"Status"`
|
||||
Filter Filter `xml:"Filter"`
|
||||
Expiration Expiration `xml:"Expiration,omitempty"`
|
||||
Transition Transition `xml:"Transition,omitempty"`
|
||||
// FIXME: add a type to catch unsupported AbortIncompleteMultipartUpload AbortIncompleteMultipartUpload `xml:"AbortIncompleteMultipartUpload,omitempty"`
|
||||
NoncurrentVersionExpiration NoncurrentVersionExpiration `xml:"NoncurrentVersionExpiration,omitempty"`
|
||||
NoncurrentVersionTransition NoncurrentVersionTransition `xml:"NoncurrentVersionTransition,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
errInvalidRuleID = errors.New("ID must be less than 255 characters")
|
||||
errEmptyRuleStatus = errors.New("Status should not be empty")
|
||||
errInvalidRuleStatus = errors.New("Status must be set to either Enabled or Disabled")
|
||||
errMissingExpirationAction = errors.New("No expiration action found")
|
||||
)
|
||||
|
||||
// isIDValid - checks if ID is valid or not.
|
||||
func (r Rule) validateID() error {
|
||||
// cannot be longer than 255 characters
|
||||
if len(string(r.ID)) > 255 {
|
||||
return errInvalidRuleID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isStatusValid - checks if status is valid or not.
|
||||
func (r Rule) validateStatus() error {
|
||||
// Status can't be empty
|
||||
if len(r.Status) == 0 {
|
||||
return errEmptyRuleStatus
|
||||
}
|
||||
|
||||
// Status must be one of Enabled or Disabled
|
||||
if r.Status != "Enabled" && r.Status != "Disabled" {
|
||||
return errInvalidRuleStatus
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r Rule) validateAction() error {
|
||||
if r.Expiration == (Expiration{}) {
|
||||
return errMissingExpirationAction
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate - validates the rule element
|
||||
func (r Rule) Validate() error {
|
||||
if err := r.validateID(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.validateStatus(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.validateAction(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 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 lifecycle
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestUnsupportedRules checks if Rule xml with unsuported tags return
|
||||
// appropriate errors on parsing
|
||||
func TestUnsupportedRules(t *testing.T) {
|
||||
// NoncurrentVersionTransition, NoncurrentVersionExpiration
|
||||
// and Transition tags aren't supported
|
||||
unsupportedTestCases := []struct {
|
||||
inputXML string
|
||||
expectedErr error
|
||||
}{
|
||||
{ // Rule with unsupported NoncurrentVersionTransition
|
||||
inputXML: ` <Rule>
|
||||
<NoncurrentVersionTransition></NoncurrentVersionTransition>
|
||||
</Rule>`,
|
||||
expectedErr: errNoncurrentVersionTransitionUnsupported,
|
||||
},
|
||||
{ // Rule with unsupported NoncurrentVersionExpiration
|
||||
|
||||
inputXML: ` <Rule>
|
||||
<NoncurrentVersionExpiration></NoncurrentVersionExpiration>
|
||||
</Rule>`,
|
||||
expectedErr: errNoncurrentVersionExpirationUnsupported,
|
||||
},
|
||||
{ // Rule with unsupported Transition action
|
||||
inputXML: ` <Rule>
|
||||
<Transition></Transition>
|
||||
</Rule>`,
|
||||
expectedErr: errTransitionUnsupported,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range unsupportedTestCases {
|
||||
t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) {
|
||||
var rule Rule
|
||||
err := xml.Unmarshal([]byte(tc.inputXML), &rule)
|
||||
if err != tc.expectedErr {
|
||||
t.Fatalf("%d: Expected %v but got %v", i+1, tc.expectedErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestInvalidRules checks if Rule xml with invalid elements returns
|
||||
// appropriate errors on validation
|
||||
func TestInvalidRules(t *testing.T) {
|
||||
invalidTestCases := []struct {
|
||||
inputXML string
|
||||
expectedErr error
|
||||
}{
|
||||
{ // Rule without expiration action
|
||||
inputXML: ` <Rule>
|
||||
<Status>Enabled</Status>
|
||||
</Rule>`,
|
||||
expectedErr: errMissingExpirationAction,
|
||||
},
|
||||
{ // Rule with ID longer than 255 characters
|
||||
inputXML: ` <Rule>
|
||||
<ID> babababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab </ID>
|
||||
</Rule>`,
|
||||
expectedErr: errInvalidRuleID,
|
||||
},
|
||||
{ // Rule with empty status
|
||||
inputXML: ` <Rule>
|
||||
<Status></Status>
|
||||
</Rule>`,
|
||||
expectedErr: errEmptyRuleStatus,
|
||||
},
|
||||
{ // Rule with invalid status
|
||||
inputXML: ` <Rule>
|
||||
<Status>OK</Status>
|
||||
</Rule>`,
|
||||
expectedErr: errInvalidRuleStatus,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range invalidTestCases {
|
||||
t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) {
|
||||
var rule Rule
|
||||
err := xml.Unmarshal([]byte(tc.inputXML), &rule)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := rule.Validate(); err != tc.expectedErr {
|
||||
t.Fatalf("%d: Expected %v but got %v", i+1, tc.expectedErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 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 lifecycle
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Tag - a tag for a lifecycle configuration Rule filter.
|
||||
type Tag struct {
|
||||
XMLName xml.Name `xml:"Tag"`
|
||||
Key string `xml:"Key,omitempty"`
|
||||
Value string `xml:"Value,omitempty"`
|
||||
}
|
||||
|
||||
var errTagUnsupported = errors.New("Specifying <Tag></Tag> is not supported")
|
||||
|
||||
// UnmarshalXML is extended to indicate lack of support for Tag
|
||||
// xml tag in object lifecycle configuration
|
||||
func (t Tag) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
return errTagUnsupported
|
||||
}
|
||||
|
||||
// MarshalXML is extended to leave out <Tag></Tag> tags
|
||||
func (t Tag) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 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 lifecycle
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Transition - transition actions for a rule in lifecycle configuration.
|
||||
type Transition struct {
|
||||
XMLName xml.Name `xml:"Transition"`
|
||||
Days int `xml:"Days,omitempty"`
|
||||
Date string `xml:"Date,omitempty"`
|
||||
StorageClass string `xml:"StorageClass"`
|
||||
}
|
||||
|
||||
var errTransitionUnsupported = errors.New("Specifying <Transition></Transition> tag is not supported")
|
||||
|
||||
// UnmarshalXML is extended to indicate lack of support for Transition
|
||||
// xml tag in object lifecycle configuration
|
||||
func (t Transition) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
return errTransitionUnsupported
|
||||
}
|
||||
|
||||
// MarshalXML is extended to leave out <Transition></Transition> tags
|
||||
func (t Transition) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
return nil
|
||||
}
|
||||
@@ -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 lock
|
||||
|
||||
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 lock.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 lock
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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 tagging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Error is the generic type for any error happening during tag
|
||||
// parsing.
|
||||
type Error struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// Errorf - formats according to a format specifier and returns
|
||||
// the string as a value that satisfies error of type tagging.Error
|
||||
func Errorf(format string, a ...interface{}) error {
|
||||
return Error{err: fmt.Errorf(format, a...)}
|
||||
}
|
||||
|
||||
// Unwrap the internal error.
|
||||
func (e Error) Unwrap() error { return e.err }
|
||||
|
||||
// Error 'error' compatible method.
|
||||
func (e Error) Error() string {
|
||||
if e.err == nil {
|
||||
return "tagging: cause <nil>"
|
||||
}
|
||||
return e.err.Error()
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 tagging
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Tag - single tag
|
||||
type Tag struct {
|
||||
XMLName xml.Name `xml:"Tag"`
|
||||
Key string `xml:"Key"`
|
||||
Value string `xml:"Value"`
|
||||
}
|
||||
|
||||
// Validate - validates the tag element
|
||||
func (t Tag) Validate() error {
|
||||
if err := t.validateKey(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := t.validateValue(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateKey - checks if key is valid or not.
|
||||
func (t Tag) validateKey() error {
|
||||
// cannot be longer than maxTagKeyLength characters
|
||||
if utf8.RuneCountInString(t.Key) > maxTagKeyLength {
|
||||
return ErrInvalidTagKey
|
||||
}
|
||||
// cannot be empty
|
||||
if len(t.Key) == 0 {
|
||||
return ErrInvalidTagKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateValue - checks if value is valid or not.
|
||||
func (t Tag) validateValue() error {
|
||||
// cannot be longer than maxTagValueLength characters
|
||||
if utf8.RuneCountInString(t.Value) > maxTagValueLength {
|
||||
return ErrInvalidTagValue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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 tagging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// S3 API limits for tags
|
||||
// Ref: https://docs.aws.amazon.com/AmazonS3/latest/dev/object-tagging.html
|
||||
const (
|
||||
maxTags = 10
|
||||
maxTagKeyLength = 128
|
||||
maxTagValueLength = 256
|
||||
)
|
||||
|
||||
// errors returned by tagging package
|
||||
var (
|
||||
ErrTooManyTags = Errorf("Cannot have more than 10 object tags")
|
||||
ErrInvalidTagKey = Errorf("The TagKey you have provided is invalid")
|
||||
ErrInvalidTagValue = Errorf("The TagValue you have provided is invalid")
|
||||
ErrInvalidTag = Errorf("Cannot provide multiple Tags with the same key")
|
||||
)
|
||||
|
||||
// Tagging - object tagging interface
|
||||
type Tagging struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Tagging"`
|
||||
TagSet TagSet `xml:"TagSet"`
|
||||
}
|
||||
|
||||
// Validate - validates the tagging configuration
|
||||
func (t Tagging) Validate() error {
|
||||
// Tagging can't have more than 10 tags
|
||||
if len(t.TagSet.Tags) > maxTags {
|
||||
return ErrTooManyTags
|
||||
}
|
||||
// Validate all the rules in the tagging config
|
||||
for _, ts := range t.TagSet.Tags {
|
||||
if t.TagSet.ContainsDuplicate(ts.Key) {
|
||||
return ErrInvalidTag
|
||||
}
|
||||
if err := ts.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// String - returns a string in format "tag1=value1&tag2=value2" with all the
|
||||
// tags in this Tagging Struct
|
||||
func (t Tagging) String() string {
|
||||
var buf bytes.Buffer
|
||||
for _, tag := range t.TagSet.Tags {
|
||||
if buf.Len() > 0 {
|
||||
buf.WriteString("&")
|
||||
}
|
||||
buf.WriteString(tag.Key + "=")
|
||||
buf.WriteString(tag.Value)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// FromString - returns a Tagging struct when given a string in format
|
||||
// "tag1=value1&tag2=value2"
|
||||
func FromString(tagStr string) (Tagging, error) {
|
||||
tags, err := url.ParseQuery(tagStr)
|
||||
if err != nil {
|
||||
return Tagging{}, err
|
||||
}
|
||||
var idx = 0
|
||||
parsedTags := make([]Tag, len(tags))
|
||||
for k := range tags {
|
||||
parsedTags[idx].Key = k
|
||||
parsedTags[idx].Value = tags.Get(k)
|
||||
idx++
|
||||
}
|
||||
return Tagging{
|
||||
TagSet: TagSet{
|
||||
Tags: parsedTags,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseTagging - parses incoming xml data in given reader
|
||||
// into Tagging interface. After parsing, also validates the
|
||||
// parsed fields based on S3 API constraints.
|
||||
func ParseTagging(reader io.Reader) (*Tagging, error) {
|
||||
var t Tagging
|
||||
if err := xml.NewDecoder(reader).Decode(&t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := t.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 tagging
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// TagSet - Set of tags under Tagging
|
||||
type TagSet struct {
|
||||
XMLName xml.Name `xml:"TagSet"`
|
||||
Tags []Tag `xml:"Tag"`
|
||||
}
|
||||
|
||||
// ContainsDuplicate - returns true if duplicate keys are present in TagSet
|
||||
func (t TagSet) ContainsDuplicate(key string) bool {
|
||||
var found bool
|
||||
for _, tag := range t.Tags {
|
||||
if tag.Key == key {
|
||||
if found {
|
||||
return true
|
||||
}
|
||||
found = true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/minio/minio/pkg/bucket/policy/condition"
|
||||
)
|
||||
|
||||
// Action - policy action.
|
||||
// Refer https://docs.aws.amazon.com/IAM/latest/UserGuide/list_amazons3.html
|
||||
// for more information about available actions.
|
||||
type Action string
|
||||
|
||||
const (
|
||||
// AbortMultipartUploadAction - AbortMultipartUpload Rest API action.
|
||||
AbortMultipartUploadAction Action = "s3:AbortMultipartUpload"
|
||||
|
||||
// CreateBucketAction - CreateBucket Rest API action.
|
||||
CreateBucketAction = "s3:CreateBucket"
|
||||
|
||||
// DeleteBucketAction - DeleteBucket Rest API action.
|
||||
DeleteBucketAction = "s3:DeleteBucket"
|
||||
|
||||
// DeleteBucketPolicyAction - DeleteBucketPolicy Rest API action.
|
||||
DeleteBucketPolicyAction = "s3:DeleteBucketPolicy"
|
||||
|
||||
// DeleteObjectAction - DeleteObject Rest API action.
|
||||
DeleteObjectAction = "s3:DeleteObject"
|
||||
|
||||
// GetBucketLocationAction - GetBucketLocation Rest API action.
|
||||
GetBucketLocationAction = "s3:GetBucketLocation"
|
||||
|
||||
// GetBucketNotificationAction - GetBucketNotification Rest API action.
|
||||
GetBucketNotificationAction = "s3:GetBucketNotification"
|
||||
|
||||
// GetBucketPolicyAction - GetBucketPolicy Rest API action.
|
||||
GetBucketPolicyAction = "s3:GetBucketPolicy"
|
||||
|
||||
// GetObjectAction - GetObject Rest API action.
|
||||
GetObjectAction = "s3:GetObject"
|
||||
|
||||
// HeadBucketAction - HeadBucket Rest API action. This action is unused in minio.
|
||||
HeadBucketAction = "s3:HeadBucket"
|
||||
|
||||
// ListAllMyBucketsAction - ListAllMyBuckets (List buckets) Rest API action.
|
||||
ListAllMyBucketsAction = "s3:ListAllMyBuckets"
|
||||
|
||||
// ListBucketAction - ListBucket Rest API action.
|
||||
ListBucketAction = "s3:ListBucket"
|
||||
|
||||
// ListBucketMultipartUploadsAction - ListMultipartUploads Rest API action.
|
||||
ListBucketMultipartUploadsAction = "s3:ListBucketMultipartUploads"
|
||||
|
||||
// ListenBucketNotificationAction - ListenBucketNotification Rest API action.
|
||||
// This is MinIO extension.
|
||||
ListenBucketNotificationAction = "s3:ListenBucketNotification"
|
||||
|
||||
// ListMultipartUploadPartsAction - ListParts Rest API action.
|
||||
ListMultipartUploadPartsAction = "s3:ListMultipartUploadParts"
|
||||
|
||||
// PutBucketNotificationAction - PutObjectNotification Rest API action.
|
||||
PutBucketNotificationAction = "s3:PutBucketNotification"
|
||||
|
||||
// PutBucketPolicyAction - PutBucketPolicy Rest API action.
|
||||
PutBucketPolicyAction = "s3:PutBucketPolicy"
|
||||
|
||||
// PutObjectAction - PutObject Rest API action.
|
||||
PutObjectAction = "s3:PutObject"
|
||||
|
||||
// PutBucketLifecycleAction - PutBucketLifecycle Rest API action.
|
||||
PutBucketLifecycleAction = "s3:PutLifecycleConfiguration"
|
||||
|
||||
// GetBucketLifecycleAction - GetBucketLifecycle Rest API action.
|
||||
GetBucketLifecycleAction = "s3:GetLifecycleConfiguration"
|
||||
|
||||
// BypassGovernanceModeAction - bypass governance mode for DeleteObject Rest API action.
|
||||
BypassGovernanceModeAction = "s3:BypassGovernanceMode"
|
||||
// BypassGovernanceRetentionAction - bypass governance retention for PutObjectRetention, PutObject and DeleteObject Rest API action.
|
||||
BypassGovernanceRetentionAction = "s3:BypassGovernanceRetention"
|
||||
// PutObjectRetentionAction - PutObjectRetention Rest API action.
|
||||
PutObjectRetentionAction = "s3:PutObjectRetention"
|
||||
|
||||
// GetObjectRetentionAction - GetObjectRetention, GetObject, HeadObject Rest API action.
|
||||
GetObjectRetentionAction = "s3:GetObjectRetention"
|
||||
// GetObjectLegalHoldAction - GetObjectLegalHold, GetObject Rest API action.
|
||||
GetObjectLegalHoldAction = "s3:GetObjectLegalHold"
|
||||
// PutObjectLegalHoldAction - PutObjectLegalHold, PutObject Rest API action.
|
||||
PutObjectLegalHoldAction = "s3:PutObjectLegalHold"
|
||||
// GetBucketObjectLockConfigurationAction - GetObjectLockConfiguration Rest API action
|
||||
GetBucketObjectLockConfigurationAction = "s3:GetBucketObjectLockConfiguration"
|
||||
// PutBucketObjectLockConfigurationAction - PutObjectLockConfiguration Rest API action
|
||||
PutBucketObjectLockConfigurationAction = "s3:PutBucketObjectLockConfiguration"
|
||||
|
||||
// GetObjectTaggingAction - Get Object Tags API action
|
||||
GetObjectTaggingAction = "s3:GetObjectTagging"
|
||||
// PutObjectTaggingAction - Put Object Tags API action
|
||||
PutObjectTaggingAction = "s3:PutObjectTagging"
|
||||
// DeleteObjectTaggingAction - Delete Object Tags API action
|
||||
DeleteObjectTaggingAction = "s3:DeleteObjectTagging"
|
||||
)
|
||||
|
||||
// isObjectAction - returns whether action is object type or not.
|
||||
func (action Action) isObjectAction() bool {
|
||||
switch action {
|
||||
case AbortMultipartUploadAction, DeleteObjectAction, GetObjectAction:
|
||||
fallthrough
|
||||
case ListMultipartUploadPartsAction, PutObjectAction:
|
||||
return true
|
||||
case PutObjectRetentionAction, GetObjectRetentionAction:
|
||||
return true
|
||||
case PutObjectLegalHoldAction, GetObjectLegalHoldAction:
|
||||
return true
|
||||
case BypassGovernanceModeAction, BypassGovernanceRetentionAction:
|
||||
return true
|
||||
case GetObjectTaggingAction, PutObjectTaggingAction, DeleteObjectTaggingAction:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsValid - checks if action is valid or not.
|
||||
func (action Action) IsValid() bool {
|
||||
switch action {
|
||||
case AbortMultipartUploadAction, CreateBucketAction, DeleteBucketAction:
|
||||
fallthrough
|
||||
case DeleteBucketPolicyAction, DeleteObjectAction, GetBucketLocationAction:
|
||||
fallthrough
|
||||
case GetBucketNotificationAction, GetBucketPolicyAction, GetObjectAction:
|
||||
fallthrough
|
||||
case HeadBucketAction, ListAllMyBucketsAction, ListBucketAction:
|
||||
fallthrough
|
||||
case ListBucketMultipartUploadsAction, ListenBucketNotificationAction:
|
||||
fallthrough
|
||||
case ListMultipartUploadPartsAction, PutBucketNotificationAction:
|
||||
fallthrough
|
||||
case PutBucketPolicyAction, PutObjectAction:
|
||||
fallthrough
|
||||
case PutBucketLifecycleAction, GetBucketLifecycleAction:
|
||||
return true
|
||||
case BypassGovernanceModeAction, BypassGovernanceRetentionAction:
|
||||
return true
|
||||
case PutObjectRetentionAction, GetObjectRetentionAction:
|
||||
return true
|
||||
case PutObjectLegalHoldAction, GetObjectLegalHoldAction:
|
||||
return true
|
||||
case PutBucketObjectLockConfigurationAction, GetBucketObjectLockConfigurationAction:
|
||||
return true
|
||||
case GetObjectTaggingAction, PutObjectTaggingAction, DeleteObjectTaggingAction:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON - encodes Action to JSON data.
|
||||
func (action Action) MarshalJSON() ([]byte, error) {
|
||||
if action.IsValid() {
|
||||
return json.Marshal(string(action))
|
||||
}
|
||||
|
||||
return nil, Errorf("invalid action '%v'", action)
|
||||
}
|
||||
|
||||
// UnmarshalJSON - decodes JSON data to Action.
|
||||
func (action *Action) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a := Action(s)
|
||||
if !a.IsValid() {
|
||||
return Errorf("invalid action '%v'", s)
|
||||
}
|
||||
|
||||
*action = a
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseAction(s string) (Action, error) {
|
||||
action := Action(s)
|
||||
|
||||
if action.IsValid() {
|
||||
return action, nil
|
||||
}
|
||||
|
||||
return action, Errorf("unsupported action '%v'", s)
|
||||
}
|
||||
|
||||
// actionConditionKeyMap - holds mapping of supported condition key for an action.
|
||||
var actionConditionKeyMap = map[Action]condition.KeySet{
|
||||
AbortMultipartUploadAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
|
||||
CreateBucketAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
|
||||
DeleteObjectAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
|
||||
GetBucketLocationAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
|
||||
GetObjectAction: condition.NewKeySet(
|
||||
append([]condition.Key{
|
||||
condition.S3XAmzServerSideEncryption,
|
||||
condition.S3XAmzServerSideEncryptionCustomerAlgorithm,
|
||||
condition.S3XAmzStorageClass,
|
||||
}, condition.CommonKeys...)...),
|
||||
|
||||
HeadBucketAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
|
||||
ListAllMyBucketsAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
|
||||
ListBucketAction: condition.NewKeySet(
|
||||
append([]condition.Key{
|
||||
condition.S3Prefix,
|
||||
condition.S3Delimiter,
|
||||
condition.S3MaxKeys,
|
||||
}, condition.CommonKeys...)...),
|
||||
|
||||
ListBucketMultipartUploadsAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
|
||||
ListMultipartUploadPartsAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
|
||||
PutObjectAction: condition.NewKeySet(
|
||||
append([]condition.Key{
|
||||
condition.S3XAmzCopySource,
|
||||
condition.S3XAmzServerSideEncryption,
|
||||
condition.S3XAmzServerSideEncryptionCustomerAlgorithm,
|
||||
condition.S3XAmzMetadataDirective,
|
||||
condition.S3XAmzStorageClass,
|
||||
}, condition.CommonKeys...)...),
|
||||
PutObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
GetObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
BypassGovernanceModeAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
BypassGovernanceRetentionAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
PutObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
GetObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
PutObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
GetObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
DeleteObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestActionIsObjectAction(t *testing.T) {
|
||||
testCases := []struct {
|
||||
action Action
|
||||
expectedResult bool
|
||||
}{
|
||||
{AbortMultipartUploadAction, true},
|
||||
{DeleteObjectAction, true},
|
||||
{GetObjectAction, true},
|
||||
{ListMultipartUploadPartsAction, true},
|
||||
{PutObjectAction, true},
|
||||
{CreateBucketAction, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.action.isObjectAction()
|
||||
|
||||
if testCase.expectedResult != result {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionIsValid(t *testing.T) {
|
||||
testCases := []struct {
|
||||
action Action
|
||||
expectedResult bool
|
||||
}{
|
||||
{AbortMultipartUploadAction, true},
|
||||
{Action("foo"), false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.action.IsValid()
|
||||
|
||||
if testCase.expectedResult != result {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionMarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
action Action
|
||||
expectedResult []byte
|
||||
expectErr bool
|
||||
}{
|
||||
{PutObjectAction, []byte(`"s3:PutObject"`), false},
|
||||
{Action("foo"), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := json.Marshal(testCase.action)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if testCase.expectErr != expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionUnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
data []byte
|
||||
expectedResult Action
|
||||
expectErr bool
|
||||
}{
|
||||
{[]byte(`"s3:PutObject"`), PutObjectAction, false},
|
||||
{[]byte(`"foo"`), Action(""), true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
var result Action
|
||||
err := json.Unmarshal(testCase.data, &result)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if testCase.expectErr != expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if testCase.expectedResult != result {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
)
|
||||
|
||||
// ActionSet - set of actions.
|
||||
type ActionSet map[Action]struct{}
|
||||
|
||||
// Add - add action to the set.
|
||||
func (actionSet ActionSet) Add(action Action) {
|
||||
actionSet[action] = struct{}{}
|
||||
}
|
||||
|
||||
// Contains - checks given action exists in the action set.
|
||||
func (actionSet ActionSet) Contains(action Action) bool {
|
||||
_, found := actionSet[action]
|
||||
return found
|
||||
}
|
||||
|
||||
// Equals - checks whether given action set is equal to current action set or not.
|
||||
func (actionSet ActionSet) Equals(sactionSet ActionSet) bool {
|
||||
// If length of set is not equal to length of given set, the
|
||||
// set is not equal to given set.
|
||||
if len(actionSet) != len(sactionSet) {
|
||||
return false
|
||||
}
|
||||
|
||||
// As both sets are equal in length, check each elements are equal.
|
||||
for k := range actionSet {
|
||||
if _, ok := sactionSet[k]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Intersection - returns actions available in both ActionSet.
|
||||
func (actionSet ActionSet) Intersection(sset ActionSet) ActionSet {
|
||||
nset := NewActionSet()
|
||||
for k := range actionSet {
|
||||
if _, ok := sset[k]; ok {
|
||||
nset.Add(k)
|
||||
}
|
||||
}
|
||||
|
||||
return nset
|
||||
}
|
||||
|
||||
// MarshalJSON - encodes ActionSet to JSON data.
|
||||
func (actionSet ActionSet) MarshalJSON() ([]byte, error) {
|
||||
if len(actionSet) == 0 {
|
||||
return nil, Errorf("empty actions not allowed")
|
||||
}
|
||||
|
||||
return json.Marshal(actionSet.ToSlice())
|
||||
}
|
||||
|
||||
func (actionSet ActionSet) String() string {
|
||||
actions := []string{}
|
||||
for action := range actionSet {
|
||||
actions = append(actions, string(action))
|
||||
}
|
||||
sort.Strings(actions)
|
||||
|
||||
return fmt.Sprintf("%v", actions)
|
||||
}
|
||||
|
||||
// ToSlice - returns slice of actions from the action set.
|
||||
func (actionSet ActionSet) ToSlice() []Action {
|
||||
actions := []Action{}
|
||||
for action := range actionSet {
|
||||
actions = append(actions, action)
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
// UnmarshalJSON - decodes JSON data to ActionSet.
|
||||
func (actionSet *ActionSet) UnmarshalJSON(data []byte) error {
|
||||
var sset set.StringSet
|
||||
if err := json.Unmarshal(data, &sset); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(sset) == 0 {
|
||||
return Errorf("empty actions not allowed")
|
||||
}
|
||||
|
||||
*actionSet = make(ActionSet)
|
||||
for _, s := range sset.ToSlice() {
|
||||
action, err := parseAction(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actionSet.Add(action)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewActionSet - creates new action set.
|
||||
func NewActionSet(actions ...Action) ActionSet {
|
||||
actionSet := make(ActionSet)
|
||||
for _, action := range actions {
|
||||
actionSet.Add(action)
|
||||
}
|
||||
|
||||
return actionSet
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestActionSetAdd(t *testing.T) {
|
||||
testCases := []struct {
|
||||
set ActionSet
|
||||
action Action
|
||||
expectedResult ActionSet
|
||||
}{
|
||||
{NewActionSet(), PutObjectAction, NewActionSet(PutObjectAction)},
|
||||
{NewActionSet(PutObjectAction), PutObjectAction, NewActionSet(PutObjectAction)},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
testCase.set.Add(testCase.action)
|
||||
|
||||
if !reflect.DeepEqual(testCase.expectedResult, testCase.set) {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionSetContains(t *testing.T) {
|
||||
testCases := []struct {
|
||||
set ActionSet
|
||||
action Action
|
||||
expectedResult bool
|
||||
}{
|
||||
{NewActionSet(PutObjectAction), PutObjectAction, true},
|
||||
{NewActionSet(PutObjectAction, GetObjectAction), PutObjectAction, true},
|
||||
{NewActionSet(PutObjectAction, GetObjectAction), AbortMultipartUploadAction, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.set.Contains(testCase.action)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionSetIntersection(t *testing.T) {
|
||||
testCases := []struct {
|
||||
set ActionSet
|
||||
setToIntersect ActionSet
|
||||
expectedResult ActionSet
|
||||
}{
|
||||
{NewActionSet(), NewActionSet(PutObjectAction), NewActionSet()},
|
||||
{NewActionSet(PutObjectAction), NewActionSet(), NewActionSet()},
|
||||
{NewActionSet(PutObjectAction), NewActionSet(PutObjectAction, GetObjectAction), NewActionSet(PutObjectAction)},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.set.Intersection(testCase.setToIntersect)
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionSetMarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
actionSet ActionSet
|
||||
expectedResult []byte
|
||||
expectErr bool
|
||||
}{
|
||||
{NewActionSet(PutObjectAction), []byte(`["s3:PutObject"]`), false},
|
||||
{NewActionSet(), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := json.Marshal(testCase.actionSet)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, string(testCase.expectedResult), string(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionSetToSlice(t *testing.T) {
|
||||
testCases := []struct {
|
||||
actionSet ActionSet
|
||||
expectedResult []Action
|
||||
}{
|
||||
{NewActionSet(PutObjectAction), []Action{PutObjectAction}},
|
||||
{NewActionSet(), []Action{}},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.actionSet.ToSlice()
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionSetUnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
data []byte
|
||||
expectedResult ActionSet
|
||||
expectErr bool
|
||||
}{
|
||||
{[]byte(`"s3:PutObject"`), NewActionSet(PutObjectAction), false},
|
||||
{[]byte(`["s3:PutObject"]`), NewActionSet(PutObjectAction), false},
|
||||
{[]byte(`["s3:PutObject", "s3:GetObject"]`), NewActionSet(PutObjectAction, GetObjectAction), false},
|
||||
{[]byte(`["s3:PutObject", "s3:GetObject", "s3:PutObject"]`), NewActionSet(PutObjectAction, GetObjectAction), false},
|
||||
{[]byte(`[]`), NewActionSet(), true}, // Empty array.
|
||||
{[]byte(`"foo"`), nil, true}, // Invalid action.
|
||||
{[]byte(`["s3:PutObject", "foo"]`), nil, true}, // Invalid action.
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := make(ActionSet)
|
||||
err := json.Unmarshal(testCase.data, &result)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
)
|
||||
|
||||
func toBinaryEqualsFuncString(n name, key Key, values set.StringSet) string {
|
||||
valueStrings := values.ToSlice()
|
||||
sort.Strings(valueStrings)
|
||||
|
||||
return fmt.Sprintf("%v:%v:%v", n, key, valueStrings)
|
||||
}
|
||||
|
||||
// binaryEqualsFunc - String equals function. It checks whether value by Key in given
|
||||
// values map is in condition values.
|
||||
// For example,
|
||||
// - if values = ["mybucket/foo"], at evaluate() it returns whether string
|
||||
// in value map for Key is in values.
|
||||
type binaryEqualsFunc struct {
|
||||
k Key
|
||||
values set.StringSet
|
||||
}
|
||||
|
||||
// evaluate() - evaluates to check whether value by Key in given values is in
|
||||
// condition values.
|
||||
func (f binaryEqualsFunc) evaluate(values map[string][]string) bool {
|
||||
requestValue, ok := values[http.CanonicalHeaderKey(f.k.Name())]
|
||||
if !ok {
|
||||
requestValue = values[f.k.Name()]
|
||||
}
|
||||
|
||||
fvalues := f.values.ApplyFunc(substFuncFromValues(values))
|
||||
return !fvalues.Intersection(set.CreateStringSet(requestValue...)).IsEmpty()
|
||||
}
|
||||
|
||||
// key() - returns condition key which is used by this condition function.
|
||||
func (f binaryEqualsFunc) key() Key {
|
||||
return f.k
|
||||
}
|
||||
|
||||
// name() - returns "BinaryEquals" condition name.
|
||||
func (f binaryEqualsFunc) name() name {
|
||||
return binaryEquals
|
||||
}
|
||||
|
||||
func (f binaryEqualsFunc) String() string {
|
||||
return toBinaryEqualsFuncString(binaryEquals, f.k, f.values)
|
||||
}
|
||||
|
||||
// toMap - returns map representation of this function.
|
||||
func (f binaryEqualsFunc) toMap() map[Key]ValueSet {
|
||||
if !f.k.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
values := NewValueSet()
|
||||
for _, value := range f.values.ToSlice() {
|
||||
values.Add(NewStringValue(base64.StdEncoding.EncodeToString([]byte(value))))
|
||||
}
|
||||
|
||||
return map[Key]ValueSet{
|
||||
f.k: values,
|
||||
}
|
||||
}
|
||||
|
||||
func validateBinaryEqualsValues(n name, key Key, values set.StringSet) error {
|
||||
vslice := values.ToSlice()
|
||||
for _, s := range vslice {
|
||||
sbytes, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
values.Remove(s)
|
||||
s = string(sbytes)
|
||||
switch key {
|
||||
case S3XAmzCopySource:
|
||||
bucket, object := path2BucketAndObject(s)
|
||||
if object == "" {
|
||||
return fmt.Errorf("invalid value '%v' for '%v' for %v condition", s, S3XAmzCopySource, n)
|
||||
}
|
||||
if err = s3utils.CheckValidBucketName(bucket); err != nil {
|
||||
return err
|
||||
}
|
||||
case S3XAmzServerSideEncryption, S3XAmzServerSideEncryptionCustomerAlgorithm:
|
||||
if s != "AES256" {
|
||||
return fmt.Errorf("invalid value '%v' for '%v' for %v condition", s, S3XAmzServerSideEncryption, n)
|
||||
}
|
||||
case S3XAmzMetadataDirective:
|
||||
if s != "COPY" && s != "REPLACE" {
|
||||
return fmt.Errorf("invalid value '%v' for '%v' for %v condition", s, S3XAmzMetadataDirective, n)
|
||||
}
|
||||
}
|
||||
values.Add(s)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// newBinaryEqualsFunc - returns new BinaryEquals function.
|
||||
func newBinaryEqualsFunc(key Key, values ValueSet) (Function, error) {
|
||||
valueStrings, err := valuesToStringSlice(binaryEquals, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewBinaryEqualsFunc(key, valueStrings...)
|
||||
}
|
||||
|
||||
// NewBinaryEqualsFunc - returns new BinaryEquals function.
|
||||
func NewBinaryEqualsFunc(key Key, values ...string) (Function, error) {
|
||||
sset := set.CreateStringSet(values...)
|
||||
if err := validateBinaryEqualsValues(binaryEquals, key, sset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &binaryEqualsFunc{key, sset}, nil
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBinaryEqualsFuncEvaluate(t *testing.T) {
|
||||
case1Function, err := newBinaryEqualsFunc(S3XAmzCopySource,
|
||||
NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject")))))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newBinaryEqualsFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256")))))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newBinaryEqualsFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE")))))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newBinaryEqualsFunc(S3LocationConstraint,
|
||||
NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1")))))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
values map[string][]string
|
||||
expectedResult bool
|
||||
}{
|
||||
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, true},
|
||||
{case1Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, false},
|
||||
{case1Function, map[string][]string{}, false},
|
||||
{case1Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
|
||||
{case2Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, true},
|
||||
{case2Function, map[string][]string{}, false},
|
||||
{case2Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
|
||||
{case3Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, true},
|
||||
{case3Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, false},
|
||||
{case3Function, map[string][]string{}, false},
|
||||
{case3Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
|
||||
{case4Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, true},
|
||||
{case4Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, false},
|
||||
{case4Function, map[string][]string{}, false},
|
||||
{case4Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.evaluate(testCase.values)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBinaryEqualsFuncKey(t *testing.T) {
|
||||
case1Function, err := newBinaryEqualsFunc(S3XAmzCopySource,
|
||||
NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject")))))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newBinaryEqualsFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256")))))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newBinaryEqualsFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE")))))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newBinaryEqualsFunc(S3LocationConstraint,
|
||||
NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1")))))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
expectedResult Key
|
||||
}{
|
||||
{case1Function, S3XAmzCopySource},
|
||||
{case2Function, S3XAmzServerSideEncryption},
|
||||
{case3Function, S3XAmzMetadataDirective},
|
||||
{case4Function, S3LocationConstraint},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.key()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBinaryEqualsFuncToMap(t *testing.T) {
|
||||
case1Function, err := newBinaryEqualsFunc(S3XAmzCopySource,
|
||||
NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject")))))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case1Result := map[Key]ValueSet{
|
||||
S3XAmzCopySource: NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject")))),
|
||||
}
|
||||
|
||||
case2Function, err := newBinaryEqualsFunc(S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject"))),
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("yourbucket/myobject"))),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Result := map[Key]ValueSet{
|
||||
S3XAmzCopySource: NewValueSet(
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject"))),
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("yourbucket/myobject"))),
|
||||
),
|
||||
}
|
||||
|
||||
case3Function, err := newBinaryEqualsFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256")))))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Result := map[Key]ValueSet{
|
||||
S3XAmzServerSideEncryption: NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256")))),
|
||||
}
|
||||
|
||||
case4Function, err := newBinaryEqualsFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256"))),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Result := map[Key]ValueSet{
|
||||
S3XAmzServerSideEncryption: NewValueSet(
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256"))),
|
||||
),
|
||||
}
|
||||
|
||||
case5Function, err := newBinaryEqualsFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE")))))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case5Result := map[Key]ValueSet{
|
||||
S3XAmzMetadataDirective: NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE")))),
|
||||
}
|
||||
|
||||
case6Function, err := newBinaryEqualsFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE"))),
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("COPY"))),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case6Result := map[Key]ValueSet{
|
||||
S3XAmzMetadataDirective: NewValueSet(
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE"))),
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("COPY"))),
|
||||
),
|
||||
}
|
||||
|
||||
case7Function, err := newBinaryEqualsFunc(S3LocationConstraint,
|
||||
NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1")))))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case7Result := map[Key]ValueSet{
|
||||
S3LocationConstraint: NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1")))),
|
||||
}
|
||||
|
||||
case8Function, err := newBinaryEqualsFunc(S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1"))),
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("us-west-1"))),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case8Result := map[Key]ValueSet{
|
||||
S3LocationConstraint: NewValueSet(
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1"))),
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("us-west-1"))),
|
||||
),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
f Function
|
||||
expectedResult map[Key]ValueSet
|
||||
}{
|
||||
{case1Function, case1Result},
|
||||
{case2Function, case2Result},
|
||||
{case3Function, case3Result},
|
||||
{case4Function, case4Result},
|
||||
{case5Function, case5Result},
|
||||
{case6Function, case6Result},
|
||||
{case7Function, case7Result},
|
||||
{case8Function, case8Result},
|
||||
{&binaryEqualsFunc{}, nil},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.f.toMap()
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewBinaryEqualsFunc(t *testing.T) {
|
||||
case1Function, err := newBinaryEqualsFunc(S3XAmzCopySource,
|
||||
NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject")))))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newBinaryEqualsFunc(S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject"))),
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("yourbucket/myobject"))),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newBinaryEqualsFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256")))))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newBinaryEqualsFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256"))),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case5Function, err := newBinaryEqualsFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE")))))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case6Function, err := newBinaryEqualsFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE"))),
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("COPY"))),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case7Function, err := newBinaryEqualsFunc(S3LocationConstraint,
|
||||
NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1")))))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case8Function, err := newBinaryEqualsFunc(S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1"))),
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("us-west-1"))),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
key Key
|
||||
values ValueSet
|
||||
expectedResult Function
|
||||
expectErr bool
|
||||
}{
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject")))), case1Function, false},
|
||||
{S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject"))),
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("yourbucket/myobject"))),
|
||||
), case2Function, false},
|
||||
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256")))), case3Function, false},
|
||||
{S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256"))),
|
||||
), case4Function, false},
|
||||
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE")))), case5Function, false},
|
||||
{S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE"))),
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("COPY"))),
|
||||
), case6Function, false},
|
||||
|
||||
{S3LocationConstraint, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1")))), case7Function, false},
|
||||
{S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1"))),
|
||||
NewStringValue(base64.StdEncoding.EncodeToString([]byte("us-west-1"))),
|
||||
), case8Function, false},
|
||||
|
||||
// Unsupported value error.
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket/myobject"))), NewIntValue(7)), nil, true},
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("AES256"))), NewIntValue(7)), nil, true},
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("REPLACE"))), NewIntValue(7)), nil, true},
|
||||
{S3LocationConstraint, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("eu-west-1"))), NewIntValue(7)), nil, true},
|
||||
|
||||
// Invalid value error.
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("mybucket")))), nil, true},
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("SSE-C")))), nil, true},
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue(base64.StdEncoding.EncodeToString([]byte("DUPLICATE")))), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := newBinaryEqualsFunc(testCase.key, testCase.values)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// booleanFunc - Bool condition function. It checks whether Key is true or false.
|
||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html#Conditions_Boolean
|
||||
type booleanFunc struct {
|
||||
k Key
|
||||
value string
|
||||
}
|
||||
|
||||
// evaluate() - evaluates to check whether Key is present in given values or not.
|
||||
// Depending on condition boolean value, this function returns true or false.
|
||||
func (f booleanFunc) evaluate(values map[string][]string) bool {
|
||||
requestValue, ok := values[http.CanonicalHeaderKey(f.k.Name())]
|
||||
if !ok {
|
||||
requestValue = values[f.k.Name()]
|
||||
}
|
||||
|
||||
return f.value == requestValue[0]
|
||||
}
|
||||
|
||||
// key() - returns condition key which is used by this condition function.
|
||||
func (f booleanFunc) key() Key {
|
||||
return f.k
|
||||
}
|
||||
|
||||
// name() - returns "Bool" condition name.
|
||||
func (f booleanFunc) name() name {
|
||||
return boolean
|
||||
}
|
||||
|
||||
func (f booleanFunc) String() string {
|
||||
return fmt.Sprintf("%v:%v:%v", boolean, f.k, f.value)
|
||||
}
|
||||
|
||||
// toMap - returns map representation of this function.
|
||||
func (f booleanFunc) toMap() map[Key]ValueSet {
|
||||
if !f.k.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[Key]ValueSet{
|
||||
f.k: NewValueSet(NewStringValue(f.value)),
|
||||
}
|
||||
}
|
||||
|
||||
func newBooleanFunc(key Key, values ValueSet) (Function, error) {
|
||||
if key != AWSSecureTransport {
|
||||
return nil, fmt.Errorf("only %v key is allowed for %v condition", AWSSecureTransport, boolean)
|
||||
}
|
||||
|
||||
if len(values) != 1 {
|
||||
return nil, fmt.Errorf("only one value is allowed for boolean condition")
|
||||
}
|
||||
|
||||
var value Value
|
||||
for v := range values {
|
||||
value = v
|
||||
switch v.GetType() {
|
||||
case reflect.Bool:
|
||||
if _, err := v.GetBool(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case reflect.String:
|
||||
s, err := v.GetString()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = strconv.ParseBool(s); err != nil {
|
||||
return nil, fmt.Errorf("value must be a boolean string for boolean condition")
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("value must be a boolean for boolean condition")
|
||||
}
|
||||
}
|
||||
|
||||
return &booleanFunc{key, value.String()}, nil
|
||||
}
|
||||
|
||||
// NewBoolFunc - returns new Bool function.
|
||||
func NewBoolFunc(key Key, value string) (Function, error) {
|
||||
return &booleanFunc{key, value}, nil
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBooleanFuncEvaluate(t *testing.T) {
|
||||
case1Function, err := newBooleanFunc(AWSSecureTransport, NewValueSet(NewBoolValue(true)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newBooleanFunc(AWSSecureTransport, NewValueSet(NewBoolValue(false)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
values map[string][]string
|
||||
expectedResult bool
|
||||
}{
|
||||
{case1Function, map[string][]string{"SecureTransport": {"true"}}, true},
|
||||
{case2Function, map[string][]string{"SecureTransport": {"false"}}, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.evaluate(testCase.values)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Errorf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBooleanFuncKey(t *testing.T) {
|
||||
case1Function, err := newBooleanFunc(AWSSecureTransport, NewValueSet(NewBoolValue(true)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
expectedResult Key
|
||||
}{
|
||||
{case1Function, AWSSecureTransport},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.key()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBooleanFuncToMap(t *testing.T) {
|
||||
case1Function, err := newBooleanFunc(AWSSecureTransport, NewValueSet(NewBoolValue(true)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case1Result := map[Key]ValueSet{
|
||||
AWSSecureTransport: NewValueSet(NewStringValue("true")),
|
||||
}
|
||||
|
||||
case2Function, err := newBooleanFunc(AWSSecureTransport, NewValueSet(NewBoolValue(false)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Result := map[Key]ValueSet{
|
||||
AWSSecureTransport: NewValueSet(NewStringValue("false")),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
f Function
|
||||
expectedResult map[Key]ValueSet
|
||||
}{
|
||||
{case1Function, case1Result},
|
||||
{case2Function, case2Result},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.f.toMap()
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewBooleanFunc(t *testing.T) {
|
||||
case1Function, err := newBooleanFunc(AWSSecureTransport, NewValueSet(NewBoolValue(true)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newBooleanFunc(AWSSecureTransport, NewValueSet(NewBoolValue(false)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
key Key
|
||||
values ValueSet
|
||||
expectedResult Function
|
||||
expectErr bool
|
||||
}{
|
||||
{AWSSecureTransport, NewValueSet(NewBoolValue(true)), case1Function, false},
|
||||
{AWSSecureTransport, NewValueSet(NewStringValue("false")), case2Function, false},
|
||||
// Multiple values error.
|
||||
{AWSSecureTransport, NewValueSet(NewStringValue("true"), NewStringValue("false")), nil, true},
|
||||
// Invalid boolean string error.
|
||||
{AWSSecureTransport, NewValueSet(NewStringValue("foo")), nil, true},
|
||||
// Invalid value error.
|
||||
{AWSSecureTransport, NewValueSet(NewIntValue(7)), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := newBooleanFunc(testCase.key, testCase.values)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Function - condition function interface.
|
||||
type Function interface {
|
||||
// evaluate() - evaluates this condition function with given values.
|
||||
evaluate(values map[string][]string) bool
|
||||
|
||||
// key() - returns condition key used in this function.
|
||||
key() Key
|
||||
|
||||
// name() - returns condition name of this function.
|
||||
name() name
|
||||
|
||||
// String() - returns string representation of function.
|
||||
String() string
|
||||
|
||||
// toMap - returns map representation of this function.
|
||||
toMap() map[Key]ValueSet
|
||||
}
|
||||
|
||||
// Functions - list of functions.
|
||||
type Functions []Function
|
||||
|
||||
// Evaluate - evaluates all functions with given values map. Each function is evaluated
|
||||
// sequencely and next function is called only if current function succeeds.
|
||||
func (functions Functions) Evaluate(values map[string][]string) bool {
|
||||
for _, f := range functions {
|
||||
if !f.evaluate(values) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Keys - returns list of keys used in all functions.
|
||||
func (functions Functions) Keys() KeySet {
|
||||
keySet := NewKeySet()
|
||||
|
||||
for _, f := range functions {
|
||||
keySet.Add(f.key())
|
||||
}
|
||||
|
||||
return keySet
|
||||
}
|
||||
|
||||
// MarshalJSON - encodes Functions to JSON data.
|
||||
func (functions Functions) MarshalJSON() ([]byte, error) {
|
||||
nm := make(map[name]map[Key]ValueSet)
|
||||
|
||||
for _, f := range functions {
|
||||
if _, ok := nm[f.name()]; ok {
|
||||
for k, v := range f.toMap() {
|
||||
nm[f.name()][k] = v
|
||||
}
|
||||
} else {
|
||||
nm[f.name()] = f.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(nm)
|
||||
}
|
||||
|
||||
func (functions Functions) String() string {
|
||||
funcStrings := []string{}
|
||||
for _, f := range functions {
|
||||
s := fmt.Sprintf("%v", f)
|
||||
funcStrings = append(funcStrings, s)
|
||||
}
|
||||
sort.Strings(funcStrings)
|
||||
|
||||
return fmt.Sprintf("%v", funcStrings)
|
||||
}
|
||||
|
||||
var conditionFuncMap = map[name]func(Key, ValueSet) (Function, error){
|
||||
stringEquals: newStringEqualsFunc,
|
||||
stringNotEquals: newStringNotEqualsFunc,
|
||||
stringEqualsIgnoreCase: newStringEqualsIgnoreCaseFunc,
|
||||
stringNotEqualsIgnoreCase: newStringNotEqualsIgnoreCaseFunc,
|
||||
binaryEquals: newBinaryEqualsFunc,
|
||||
stringLike: newStringLikeFunc,
|
||||
stringNotLike: newStringNotLikeFunc,
|
||||
ipAddress: newIPAddressFunc,
|
||||
notIPAddress: newNotIPAddressFunc,
|
||||
null: newNullFunc,
|
||||
boolean: newBooleanFunc,
|
||||
// Add new conditions here.
|
||||
}
|
||||
|
||||
// UnmarshalJSON - decodes JSON data to Functions.
|
||||
func (functions *Functions) UnmarshalJSON(data []byte) error {
|
||||
// As string kind, int kind then json.Unmarshaler is checked at
|
||||
// https://github.com/golang/go/blob/master/src/encoding/json/decode.go#L618
|
||||
// UnmarshalJSON() is not called for types extending string
|
||||
// see https://play.golang.org/p/HrSsKksHvrS, better way to do is
|
||||
// https://play.golang.org/p/y9ElWpBgVAB
|
||||
//
|
||||
// Due to this issue, name and Key types cannot be used as map keys below.
|
||||
nm := make(map[string]map[string]ValueSet)
|
||||
if err := json.Unmarshal(data, &nm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(nm) == 0 {
|
||||
return fmt.Errorf("condition must not be empty")
|
||||
}
|
||||
|
||||
funcs := []Function{}
|
||||
for nameString, args := range nm {
|
||||
n, err := parseName(nameString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for keyString, values := range args {
|
||||
key, err := parseKey(keyString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vfn, ok := conditionFuncMap[n]
|
||||
if !ok {
|
||||
return fmt.Errorf("condition %v is not handled", n)
|
||||
}
|
||||
|
||||
f, err := vfn(key, values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
funcs = append(funcs, f)
|
||||
}
|
||||
}
|
||||
|
||||
*functions = funcs
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GobEncode - encodes Functions to gob data.
|
||||
func (functions Functions) GobEncode() ([]byte, error) {
|
||||
return functions.MarshalJSON()
|
||||
}
|
||||
|
||||
// GobDecode - decodes gob data to Functions.
|
||||
func (functions *Functions) GobDecode(data []byte) error {
|
||||
return functions.UnmarshalJSON(data)
|
||||
}
|
||||
|
||||
// NewFunctions - returns new Functions with given function list.
|
||||
func NewFunctions(functions ...Function) Functions {
|
||||
return Functions(functions)
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFunctionsEvaluate(t *testing.T) {
|
||||
func1, err := newNullFunc(S3XAmzCopySource, NewValueSet(NewBoolValue(true)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func2, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func3, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func4, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case1Function := NewFunctions(func1, func2, func3, func4)
|
||||
|
||||
testCases := []struct {
|
||||
functions Functions
|
||||
values map[string][]string
|
||||
expectedResult bool
|
||||
}{
|
||||
{case1Function, map[string][]string{
|
||||
"x-amz-copy-source": {"mybucket/myobject"},
|
||||
"SourceIp": {"192.168.1.10"},
|
||||
}, false},
|
||||
{case1Function, map[string][]string{
|
||||
"x-amz-copy-source": {"mybucket/myobject"},
|
||||
"SourceIp": {"192.168.1.10"},
|
||||
"Refer": {"http://example.org/"},
|
||||
}, false},
|
||||
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, false},
|
||||
{case1Function, map[string][]string{"SourceIp": {"192.168.1.10"}}, false},
|
||||
{case1Function, map[string][]string{
|
||||
"x-amz-copy-source": {"mybucket/yourobject"},
|
||||
"SourceIp": {"192.168.1.10"},
|
||||
}, false},
|
||||
{case1Function, map[string][]string{
|
||||
"x-amz-copy-source": {"mybucket/myobject"},
|
||||
"SourceIp": {"192.168.2.10"},
|
||||
}, false},
|
||||
{case1Function, map[string][]string{
|
||||
"x-amz-copy-source": {"mybucket/myobject"},
|
||||
"Refer": {"http://example.org/"},
|
||||
}, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.functions.Evaluate(testCase.values)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Errorf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFunctionsKeys(t *testing.T) {
|
||||
func1, err := newNullFunc(S3XAmzCopySource, NewValueSet(NewBoolValue(true)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func2, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func3, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func4, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
functions Functions
|
||||
expectedResult KeySet
|
||||
}{
|
||||
{NewFunctions(func1, func2, func3, func4), NewKeySet(S3XAmzCopySource, AWSSourceIP)},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.functions.Keys()
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFunctionsMarshalJSON(t *testing.T) {
|
||||
func1, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func2, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func3, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func4, err := newNotIPAddressFunc(AWSSourceIP,
|
||||
NewValueSet(NewStringValue("10.1.10.0/24")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func5, err := newStringNotLikeFunc(S3XAmzStorageClass, NewValueSet(NewStringValue("STANDARD")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func6, err := newNullFunc(S3XAmzServerSideEncryptionCustomerAlgorithm, NewValueSet(NewBoolValue(true)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func7, err := newIPAddressFunc(AWSSourceIP,
|
||||
NewValueSet(NewStringValue("192.168.1.0/24")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case1Result := []byte(`{"IpAddress":{"aws:SourceIp":["192.168.1.0/24"]},"NotIpAddress":{"aws:SourceIp":["10.1.10.0/24"]},"Null":{"s3:x-amz-server-side-encryption-customer-algorithm":[true]},"StringEquals":{"s3:x-amz-copy-source":["mybucket/myobject"]},"StringLike":{"s3:x-amz-metadata-directive":["REPL*"]},"StringNotEquals":{"s3:x-amz-server-side-encryption":["AES256"]},"StringNotLike":{"s3:x-amz-storage-class":["STANDARD"]}}`)
|
||||
|
||||
case2Result := []byte(`{"Null":{"s3:x-amz-server-side-encryption-customer-algorithm":[true]}}`)
|
||||
|
||||
testCases := []struct {
|
||||
functions Functions
|
||||
expectedResult []byte
|
||||
expectErr bool
|
||||
}{
|
||||
{NewFunctions(func1, func2, func3, func4, func5, func6, func7), case1Result, false},
|
||||
{NewFunctions(func6), case2Result, false},
|
||||
{NewFunctions(), []byte(`{}`), false},
|
||||
{nil, []byte(`{}`), false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := json.Marshal(testCase.functions)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if testCase.expectErr != expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFunctionsUnmarshalJSON(t *testing.T) {
|
||||
case1Data := []byte(`{
|
||||
"StringLike": {
|
||||
"s3:x-amz-metadata-directive": "REPL*"
|
||||
},
|
||||
"StringEquals": {
|
||||
"s3:x-amz-copy-source": "mybucket/myobject"
|
||||
},
|
||||
"StringNotEquals": {
|
||||
"s3:x-amz-server-side-encryption": "AES256"
|
||||
},
|
||||
"NotIpAddress": {
|
||||
"aws:SourceIp": [
|
||||
"10.1.10.0/24",
|
||||
"10.10.1.0/24"
|
||||
]
|
||||
},
|
||||
"StringNotLike": {
|
||||
"s3:x-amz-storage-class": "STANDARD"
|
||||
},
|
||||
"Null": {
|
||||
"s3:x-amz-server-side-encryption-customer-algorithm": true
|
||||
},
|
||||
"IpAddress": {
|
||||
"aws:SourceIp": [
|
||||
"192.168.1.0/24",
|
||||
"192.168.2.0/24"
|
||||
]
|
||||
}
|
||||
}`)
|
||||
func1, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func2, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func3, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func4, err := newNotIPAddressFunc(AWSSourceIP,
|
||||
NewValueSet(NewStringValue("10.1.10.0/24"), NewStringValue("10.10.1.0/24")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func5, err := newStringNotLikeFunc(S3XAmzStorageClass, NewValueSet(NewStringValue("STANDARD")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func6, err := newNullFunc(S3XAmzServerSideEncryptionCustomerAlgorithm, NewValueSet(NewBoolValue(true)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func7, err := newIPAddressFunc(AWSSourceIP,
|
||||
NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("192.168.2.0/24")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Data := []byte(`{
|
||||
"Null": {
|
||||
"s3:x-amz-server-side-encryption-customer-algorithm": true
|
||||
},
|
||||
"Null": {
|
||||
"s3:x-amz-server-side-encryption-customer-algorithm": "true"
|
||||
}
|
||||
}`)
|
||||
|
||||
case3Data := []byte(`{}`)
|
||||
|
||||
// Remove this test after supporting date conditions.
|
||||
case4Data := []byte(`{
|
||||
"DateEquals": { "aws:CurrentTime": "2013-06-30T00:00:00Z" }
|
||||
}`)
|
||||
|
||||
case5Data := []byte(`{
|
||||
"StringLike": {
|
||||
"s3:x-amz-metadata-directive": "REPL*"
|
||||
},
|
||||
"StringEquals": {
|
||||
"s3:x-amz-copy-source": "mybucket/myobject",
|
||||
"s3:prefix": [
|
||||
"",
|
||||
"home/"
|
||||
],
|
||||
"s3:delimiter": [
|
||||
"/"
|
||||
]
|
||||
},
|
||||
"StringNotEquals": {
|
||||
"s3:x-amz-server-side-encryption": "AES256"
|
||||
},
|
||||
"NotIpAddress": {
|
||||
"aws:SourceIp": [
|
||||
"10.1.10.0/24",
|
||||
"10.10.1.0/24"
|
||||
]
|
||||
},
|
||||
"StringNotLike": {
|
||||
"s3:x-amz-storage-class": "STANDARD"
|
||||
},
|
||||
"Null": {
|
||||
"s3:x-amz-server-side-encryption-customer-algorithm": true
|
||||
},
|
||||
"IpAddress": {
|
||||
"aws:SourceIp": [
|
||||
"192.168.1.0/24",
|
||||
"192.168.2.0/24"
|
||||
]
|
||||
}
|
||||
}`)
|
||||
|
||||
func2_1, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func2_2, err := newStringEqualsFunc(S3Prefix, NewValueSet(NewStringValue(""), NewStringValue("home/")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func2_3, err := newStringEqualsFunc(S3Delimiter, NewValueSet(NewStringValue("/")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
data []byte
|
||||
expectedResult Functions
|
||||
expectErr bool
|
||||
}{
|
||||
// Success case, basic conditions.
|
||||
{case1Data, NewFunctions(func1, func2, func3, func4, func5, func6, func7), false},
|
||||
// Duplicate conditions, success case only one value is preserved.
|
||||
{case2Data, NewFunctions(func6), false},
|
||||
// empty condition error.
|
||||
{case3Data, nil, true},
|
||||
// unsupported condition error.
|
||||
{case4Data, nil, true},
|
||||
// Success case multiple keys, same condition.
|
||||
{case5Data, NewFunctions(func1, func2_1, func2_2, func2_3, func3, func4, func5, func6, func7), false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := new(Functions)
|
||||
err := json.Unmarshal(testCase.data, result)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if testCase.expectErr != expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if (*result).String() != testCase.expectedResult.String() {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, *result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func toIPAddressFuncString(n name, key Key, values []*net.IPNet) string {
|
||||
valueStrings := []string{}
|
||||
for _, value := range values {
|
||||
valueStrings = append(valueStrings, value.String())
|
||||
}
|
||||
sort.Strings(valueStrings)
|
||||
|
||||
return fmt.Sprintf("%v:%v:%v", n, key, valueStrings)
|
||||
}
|
||||
|
||||
// ipAddressFunc - IP address function. It checks whether value by Key in given
|
||||
// values is in IP network. Here Key must be AWSSourceIP.
|
||||
// For example,
|
||||
// - if values = [192.168.1.0/24], at evaluate() it returns whether IP address
|
||||
// in value map for AWSSourceIP falls in the network 192.168.1.10/24.
|
||||
type ipAddressFunc struct {
|
||||
k Key
|
||||
values []*net.IPNet
|
||||
}
|
||||
|
||||
// evaluate() - evaluates to check whether IP address in values map for AWSSourceIP
|
||||
// falls in one of network or not.
|
||||
func (f ipAddressFunc) evaluate(values map[string][]string) bool {
|
||||
IPs := []net.IP{}
|
||||
requestValue, ok := values[http.CanonicalHeaderKey(f.k.Name())]
|
||||
if !ok {
|
||||
requestValue = values[f.k.Name()]
|
||||
}
|
||||
|
||||
for _, s := range requestValue {
|
||||
IP := net.ParseIP(s)
|
||||
if IP == nil {
|
||||
panic(fmt.Errorf("invalid IP address '%v'", s))
|
||||
}
|
||||
|
||||
IPs = append(IPs, IP)
|
||||
}
|
||||
|
||||
for _, IP := range IPs {
|
||||
for _, IPNet := range f.values {
|
||||
if IPNet.Contains(IP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// key() - returns condition key which is used by this condition function.
|
||||
// Key is always AWSSourceIP.
|
||||
func (f ipAddressFunc) key() Key {
|
||||
return f.k
|
||||
}
|
||||
|
||||
// name() - returns "IpAddress" condition name.
|
||||
func (f ipAddressFunc) name() name {
|
||||
return ipAddress
|
||||
}
|
||||
|
||||
func (f ipAddressFunc) String() string {
|
||||
return toIPAddressFuncString(ipAddress, f.k, f.values)
|
||||
}
|
||||
|
||||
// toMap - returns map representation of this function.
|
||||
func (f ipAddressFunc) toMap() map[Key]ValueSet {
|
||||
if !f.k.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
values := NewValueSet()
|
||||
for _, value := range f.values {
|
||||
values.Add(NewStringValue(value.String()))
|
||||
}
|
||||
|
||||
return map[Key]ValueSet{
|
||||
f.k: values,
|
||||
}
|
||||
}
|
||||
|
||||
// notIPAddressFunc - Not IP address function. It checks whether value by Key in given
|
||||
// values is NOT in IP network. Here Key must be AWSSourceIP.
|
||||
// For example,
|
||||
// - if values = [192.168.1.0/24], at evaluate() it returns whether IP address
|
||||
// in value map for AWSSourceIP does not fall in the network 192.168.1.10/24.
|
||||
type notIPAddressFunc struct {
|
||||
ipAddressFunc
|
||||
}
|
||||
|
||||
// evaluate() - evaluates to check whether IP address in values map for AWSSourceIP
|
||||
// does not fall in one of network.
|
||||
func (f notIPAddressFunc) evaluate(values map[string][]string) bool {
|
||||
return !f.ipAddressFunc.evaluate(values)
|
||||
}
|
||||
|
||||
// name() - returns "NotIpAddress" condition name.
|
||||
func (f notIPAddressFunc) name() name {
|
||||
return notIPAddress
|
||||
}
|
||||
|
||||
func (f notIPAddressFunc) String() string {
|
||||
return toIPAddressFuncString(notIPAddress, f.ipAddressFunc.k, f.ipAddressFunc.values)
|
||||
}
|
||||
|
||||
func valuesToIPNets(n name, values ValueSet) ([]*net.IPNet, error) {
|
||||
IPNets := []*net.IPNet{}
|
||||
for v := range values {
|
||||
s, err := v.GetString()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("value %v must be string representation of CIDR for %v condition", v, n)
|
||||
}
|
||||
|
||||
var IPNet *net.IPNet
|
||||
_, IPNet, err = net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("value %v must be CIDR string for %v condition", s, n)
|
||||
}
|
||||
|
||||
IPNets = append(IPNets, IPNet)
|
||||
}
|
||||
|
||||
return IPNets, nil
|
||||
}
|
||||
|
||||
// newIPAddressFunc - returns new IP address function.
|
||||
func newIPAddressFunc(key Key, values ValueSet) (Function, error) {
|
||||
IPNets, err := valuesToIPNets(ipAddress, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewIPAddressFunc(key, IPNets...)
|
||||
}
|
||||
|
||||
// NewIPAddressFunc - returns new IP address function.
|
||||
func NewIPAddressFunc(key Key, IPNets ...*net.IPNet) (Function, error) {
|
||||
if key != AWSSourceIP {
|
||||
return nil, fmt.Errorf("only %v key is allowed for %v condition", AWSSourceIP, ipAddress)
|
||||
}
|
||||
|
||||
return &ipAddressFunc{key, IPNets}, nil
|
||||
}
|
||||
|
||||
// newNotIPAddressFunc - returns new Not IP address function.
|
||||
func newNotIPAddressFunc(key Key, values ValueSet) (Function, error) {
|
||||
IPNets, err := valuesToIPNets(notIPAddress, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewNotIPAddressFunc(key, IPNets...)
|
||||
}
|
||||
|
||||
// NewNotIPAddressFunc - returns new Not IP address function.
|
||||
func NewNotIPAddressFunc(key Key, IPNets ...*net.IPNet) (Function, error) {
|
||||
if key != AWSSourceIP {
|
||||
return nil, fmt.Errorf("only %v key is allowed for %v condition", AWSSourceIP, notIPAddress)
|
||||
}
|
||||
|
||||
return ¬IPAddressFunc{ipAddressFunc{key, IPNets}}, nil
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIPAddressFuncEvaluate(t *testing.T) {
|
||||
case1Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
values map[string][]string
|
||||
expectedResult bool
|
||||
}{
|
||||
{case1Function, map[string][]string{"SourceIp": {"192.168.1.10"}}, true},
|
||||
{case1Function, map[string][]string{"SourceIp": {"192.168.2.10"}}, false},
|
||||
{case1Function, map[string][]string{}, false},
|
||||
{case1Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.evaluate(testCase.values)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPAddressFuncKey(t *testing.T) {
|
||||
case1Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
expectedResult Key
|
||||
}{
|
||||
{case1Function, AWSSourceIP},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.key()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPAddressFuncToMap(t *testing.T) {
|
||||
case1Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case1Result := map[Key]ValueSet{
|
||||
AWSSourceIP: NewValueSet(NewStringValue("192.168.1.0/24")),
|
||||
}
|
||||
|
||||
case2Result := map[Key]ValueSet{
|
||||
AWSSourceIP: NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
f Function
|
||||
expectedResult map[Key]ValueSet
|
||||
}{
|
||||
{case1Function, case1Result},
|
||||
{case2Function, case2Result},
|
||||
{&ipAddressFunc{}, nil},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.f.toMap()
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotIPAddressFuncEvaluate(t *testing.T) {
|
||||
case1Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
values map[string][]string
|
||||
expectedResult bool
|
||||
}{
|
||||
{case1Function, map[string][]string{"SourceIp": {"192.168.2.10"}}, true},
|
||||
{case1Function, map[string][]string{}, true},
|
||||
{case1Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
{case1Function, map[string][]string{"SourceIp": {"192.168.1.10"}}, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.evaluate(testCase.values)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotIPAddressFuncKey(t *testing.T) {
|
||||
case1Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
expectedResult Key
|
||||
}{
|
||||
{case1Function, AWSSourceIP},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.key()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotIPAddressFuncToMap(t *testing.T) {
|
||||
case1Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case1Result := map[Key]ValueSet{
|
||||
AWSSourceIP: NewValueSet(NewStringValue("192.168.1.0/24")),
|
||||
}
|
||||
|
||||
case2Result := map[Key]ValueSet{
|
||||
AWSSourceIP: NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
f Function
|
||||
expectedResult map[Key]ValueSet
|
||||
}{
|
||||
{case1Function, case1Result},
|
||||
{case2Function, case2Result},
|
||||
{¬IPAddressFunc{}, nil},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.f.toMap()
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewIPAddressFunc(t *testing.T) {
|
||||
case1Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
key Key
|
||||
values ValueSet
|
||||
expectedResult Function
|
||||
expectErr bool
|
||||
}{
|
||||
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")), case1Function, false},
|
||||
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")), case2Function, false},
|
||||
// Unsupported key error.
|
||||
{S3Prefix, NewValueSet(NewStringValue("192.168.1.0/24")), nil, true},
|
||||
// Invalid value error.
|
||||
{AWSSourceIP, NewValueSet(NewStringValue("node1.example.org")), nil, true},
|
||||
// Invalid CIDR format error.
|
||||
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0.0/24")), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := newIPAddressFunc(testCase.key, testCase.values)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if result.String() != testCase.expectedResult.String() {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewNotIPAddressFunc(t *testing.T) {
|
||||
case1Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newNotIPAddressFunc(AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
key Key
|
||||
values ValueSet
|
||||
expectedResult Function
|
||||
expectErr bool
|
||||
}{
|
||||
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24")), case1Function, false},
|
||||
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0/24"), NewStringValue("10.1.10.1/32")), case2Function, false},
|
||||
// Unsupported key error.
|
||||
{S3Prefix, NewValueSet(NewStringValue("192.168.1.0/24")), nil, true},
|
||||
// Invalid value error.
|
||||
{AWSSourceIP, NewValueSet(NewStringValue("node1.example.org")), nil, true},
|
||||
// Invalid CIDR format error.
|
||||
{AWSSourceIP, NewValueSet(NewStringValue("192.168.1.0.0/24")), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := newNotIPAddressFunc(testCase.key, testCase.values)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if result.String() != testCase.expectedResult.String() {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Key - conditional key which is used to fetch values for any condition.
|
||||
// Refer https://docs.aws.amazon.com/IAM/latest/UserGuide/list_s3.html
|
||||
// for more information about available condition keys.
|
||||
type Key string
|
||||
|
||||
const (
|
||||
// S3XAmzCopySource - key representing x-amz-copy-source HTTP header applicable to PutObject API only.
|
||||
S3XAmzCopySource Key = "s3:x-amz-copy-source"
|
||||
|
||||
// S3XAmzServerSideEncryption - key representing x-amz-server-side-encryption HTTP header applicable
|
||||
// to PutObject API only.
|
||||
S3XAmzServerSideEncryption Key = "s3:x-amz-server-side-encryption"
|
||||
|
||||
// S3XAmzServerSideEncryptionCustomerAlgorithm - key representing
|
||||
// x-amz-server-side-encryption-customer-algorithm HTTP header applicable to PutObject API only.
|
||||
S3XAmzServerSideEncryptionCustomerAlgorithm Key = "s3:x-amz-server-side-encryption-customer-algorithm"
|
||||
|
||||
// S3XAmzMetadataDirective - key representing x-amz-metadata-directive HTTP header applicable to
|
||||
// PutObject API only.
|
||||
S3XAmzMetadataDirective Key = "s3:x-amz-metadata-directive"
|
||||
|
||||
// S3XAmzStorageClass - key representing x-amz-storage-class HTTP header applicable to PutObject API
|
||||
// only.
|
||||
S3XAmzStorageClass Key = "s3:x-amz-storage-class"
|
||||
|
||||
// S3LocationConstraint - key representing LocationConstraint XML tag of CreateBucket API only.
|
||||
S3LocationConstraint Key = "s3:LocationConstraint"
|
||||
|
||||
// S3Prefix - key representing prefix query parameter of ListBucket API only.
|
||||
S3Prefix Key = "s3:prefix"
|
||||
|
||||
// S3Delimiter - key representing delimiter query parameter of ListBucket API only.
|
||||
S3Delimiter Key = "s3:delimiter"
|
||||
|
||||
// S3MaxKeys - key representing max-keys query parameter of ListBucket API only.
|
||||
S3MaxKeys Key = "s3:max-keys"
|
||||
|
||||
// AWSReferer - key representing Referer header of any API.
|
||||
AWSReferer Key = "aws:Referer"
|
||||
|
||||
// AWSSourceIP - key representing client's IP address (not intermittent proxies) of any API.
|
||||
AWSSourceIP Key = "aws:SourceIp"
|
||||
|
||||
// AWSUserAgent - key representing UserAgent header for any API.
|
||||
AWSUserAgent Key = "aws:UserAgent"
|
||||
|
||||
// AWSSecureTransport - key representing if the clients request is authenticated or not.
|
||||
AWSSecureTransport Key = "aws:SecureTransport"
|
||||
|
||||
// AWSCurrentTime - key representing the current time.
|
||||
AWSCurrentTime Key = "aws:CurrentTime"
|
||||
|
||||
// AWSEpochTime - key representing the current epoch time.
|
||||
AWSEpochTime Key = "aws:EpochTime"
|
||||
|
||||
// AWSPrincipalType - user principal type currently supported values are "User" and "Anonymous".
|
||||
AWSPrincipalType Key = "aws:principaltype"
|
||||
|
||||
// AWSUserID - user unique ID, in MinIO this value is same as your user Access Key.
|
||||
AWSUserID Key = "aws:userid"
|
||||
|
||||
// AWSUsername - user friendly name, in MinIO this value is same as your user Access Key.
|
||||
AWSUsername Key = "aws:username"
|
||||
|
||||
// JWTSub - JWT subject claim substitution.
|
||||
JWTSub Key = "jwt:sub"
|
||||
|
||||
// JWTIss issuer claim substitution.
|
||||
JWTIss Key = "jwt:iss"
|
||||
|
||||
// JWTAud audience claim substitution.
|
||||
JWTAud Key = "jwt:aud"
|
||||
|
||||
// JWTJti JWT unique identifier claim substitution.
|
||||
JWTJti Key = "jwt:jti"
|
||||
)
|
||||
|
||||
// AllSupportedKeys - is list of all all supported keys.
|
||||
var AllSupportedKeys = []Key{
|
||||
S3XAmzCopySource,
|
||||
S3XAmzServerSideEncryption,
|
||||
S3XAmzServerSideEncryptionCustomerAlgorithm,
|
||||
S3XAmzMetadataDirective,
|
||||
S3XAmzStorageClass,
|
||||
S3LocationConstraint,
|
||||
S3Prefix,
|
||||
S3Delimiter,
|
||||
S3MaxKeys,
|
||||
AWSReferer,
|
||||
AWSSourceIP,
|
||||
AWSUserAgent,
|
||||
AWSSecureTransport,
|
||||
AWSCurrentTime,
|
||||
AWSEpochTime,
|
||||
AWSPrincipalType,
|
||||
AWSUserID,
|
||||
AWSUsername,
|
||||
JWTSub,
|
||||
JWTIss,
|
||||
JWTAud,
|
||||
JWTJti,
|
||||
// Add new supported condition keys.
|
||||
}
|
||||
|
||||
// CommonKeys - is list of all common condition keys.
|
||||
var CommonKeys = []Key{
|
||||
AWSReferer,
|
||||
AWSSourceIP,
|
||||
AWSUserAgent,
|
||||
AWSSecureTransport,
|
||||
AWSCurrentTime,
|
||||
AWSEpochTime,
|
||||
AWSPrincipalType,
|
||||
AWSUserID,
|
||||
AWSUsername,
|
||||
JWTSub,
|
||||
JWTIss,
|
||||
JWTAud,
|
||||
JWTJti,
|
||||
}
|
||||
|
||||
func substFuncFromValues(values map[string][]string) func(string) string {
|
||||
return func(v string) string {
|
||||
for _, key := range CommonKeys {
|
||||
// Empty values are not supported for policy variables.
|
||||
if rvalues, ok := values[key.Name()]; ok && rvalues[0] != "" {
|
||||
v = strings.Replace(v, key.VarName(), rvalues[0], -1)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// IsValid - checks if key is valid or not.
|
||||
func (key Key) IsValid() bool {
|
||||
for _, supKey := range AllSupportedKeys {
|
||||
if supKey == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON - encodes Key to JSON data.
|
||||
func (key Key) MarshalJSON() ([]byte, error) {
|
||||
if !key.IsValid() {
|
||||
return nil, fmt.Errorf("unknown key %v", key)
|
||||
}
|
||||
|
||||
return json.Marshal(string(key))
|
||||
}
|
||||
|
||||
// VarName - returns variable key name, such as "${aws:username}"
|
||||
func (key Key) VarName() string {
|
||||
return fmt.Sprintf("${%s}", key)
|
||||
}
|
||||
|
||||
// Name - returns key name which is stripped value of prefixes "aws:" and "s3:"
|
||||
func (key Key) Name() string {
|
||||
keyString := string(key)
|
||||
|
||||
if strings.HasPrefix(keyString, "aws:") {
|
||||
return strings.TrimPrefix(keyString, "aws:")
|
||||
} else if strings.HasPrefix(keyString, "jwt:") {
|
||||
return strings.TrimPrefix(keyString, "jwt:")
|
||||
}
|
||||
return strings.TrimPrefix(keyString, "s3:")
|
||||
}
|
||||
|
||||
// UnmarshalJSON - decodes JSON data to Key.
|
||||
func (key *Key) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsedKey, err := parseKey(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*key = parsedKey
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseKey(s string) (Key, error) {
|
||||
key := Key(s)
|
||||
|
||||
if key.IsValid() {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return key, fmt.Errorf("invalid condition key '%v'", s)
|
||||
}
|
||||
|
||||
// KeySet - set representation of slice of keys.
|
||||
type KeySet map[Key]struct{}
|
||||
|
||||
// Add - add a key to key set.
|
||||
func (set KeySet) Add(key Key) {
|
||||
set[key] = struct{}{}
|
||||
}
|
||||
|
||||
// Difference - returns a key set contains difference of two keys.
|
||||
// Example:
|
||||
// keySet1 := ["one", "two", "three"]
|
||||
// keySet2 := ["two", "four", "three"]
|
||||
// keySet1.Difference(keySet2) == ["one"]
|
||||
func (set KeySet) Difference(sset KeySet) KeySet {
|
||||
nset := make(KeySet)
|
||||
|
||||
for k := range set {
|
||||
if _, ok := sset[k]; !ok {
|
||||
nset.Add(k)
|
||||
}
|
||||
}
|
||||
|
||||
return nset
|
||||
}
|
||||
|
||||
// IsEmpty - returns whether key set is empty or not.
|
||||
func (set KeySet) IsEmpty() bool {
|
||||
return len(set) == 0
|
||||
}
|
||||
|
||||
func (set KeySet) String() string {
|
||||
return fmt.Sprintf("%v", set.ToSlice())
|
||||
}
|
||||
|
||||
// ToSlice - returns slice of keys.
|
||||
func (set KeySet) ToSlice() []Key {
|
||||
keys := []Key{}
|
||||
|
||||
for key := range set {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
// NewKeySet - returns new KeySet contains given keys.
|
||||
func NewKeySet(keys ...Key) KeySet {
|
||||
set := make(KeySet)
|
||||
for _, key := range keys {
|
||||
set.Add(key)
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
// AllSupportedAdminKeys - is list of all admin supported keys.
|
||||
var AllSupportedAdminKeys = []Key{
|
||||
AWSReferer,
|
||||
AWSSourceIP,
|
||||
AWSUserAgent,
|
||||
AWSSecureTransport,
|
||||
AWSCurrentTime,
|
||||
AWSEpochTime,
|
||||
// Add new supported condition keys.
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestKeyIsValid(t *testing.T) {
|
||||
testCases := []struct {
|
||||
key Key
|
||||
expectedResult bool
|
||||
}{
|
||||
{S3XAmzCopySource, true},
|
||||
{S3XAmzServerSideEncryption, true},
|
||||
{S3XAmzServerSideEncryptionCustomerAlgorithm, true},
|
||||
{S3XAmzMetadataDirective, true},
|
||||
{S3XAmzStorageClass, true},
|
||||
{S3LocationConstraint, true},
|
||||
{S3Prefix, true},
|
||||
{S3Delimiter, true},
|
||||
{S3MaxKeys, true},
|
||||
{AWSReferer, true},
|
||||
{AWSSourceIP, true},
|
||||
{Key("foo"), false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.key.IsValid()
|
||||
|
||||
if testCase.expectedResult != result {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyMarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
key Key
|
||||
expectedResult []byte
|
||||
expectErr bool
|
||||
}{
|
||||
{S3XAmzCopySource, []byte(`"s3:x-amz-copy-source"`), false},
|
||||
{Key("foo"), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := json.Marshal(testCase.key)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if testCase.expectErr != expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: key: expected: %v, got: %v\n", i+1, string(testCase.expectedResult), string(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
key Key
|
||||
expectedResult string
|
||||
}{
|
||||
{S3XAmzCopySource, "x-amz-copy-source"},
|
||||
{AWSReferer, "Referer"},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.key.Name()
|
||||
|
||||
if testCase.expectedResult != result {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyUnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
data []byte
|
||||
expectedKey Key
|
||||
expectErr bool
|
||||
}{
|
||||
{[]byte(`"s3:x-amz-copy-source"`), S3XAmzCopySource, false},
|
||||
{[]byte(`"foo"`), Key(""), true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
var key Key
|
||||
err := json.Unmarshal(testCase.data, &key)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if testCase.expectErr != expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if testCase.expectedKey != key {
|
||||
t.Fatalf("case %v: key: expected: %v, got: %v\n", i+1, testCase.expectedKey, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeySetAdd(t *testing.T) {
|
||||
testCases := []struct {
|
||||
set KeySet
|
||||
key Key
|
||||
expectedResult KeySet
|
||||
}{
|
||||
{NewKeySet(), S3XAmzCopySource, NewKeySet(S3XAmzCopySource)},
|
||||
{NewKeySet(S3XAmzCopySource), S3XAmzCopySource, NewKeySet(S3XAmzCopySource)},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
testCase.set.Add(testCase.key)
|
||||
|
||||
if !reflect.DeepEqual(testCase.expectedResult, testCase.set) {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeySetDifference(t *testing.T) {
|
||||
testCases := []struct {
|
||||
set KeySet
|
||||
setToDiff KeySet
|
||||
expectedResult KeySet
|
||||
}{
|
||||
{NewKeySet(), NewKeySet(S3XAmzCopySource), NewKeySet()},
|
||||
{NewKeySet(S3Prefix, S3Delimiter, S3MaxKeys), NewKeySet(S3Delimiter, S3MaxKeys), NewKeySet(S3Prefix)},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.set.Difference(testCase.setToDiff)
|
||||
|
||||
if !reflect.DeepEqual(testCase.expectedResult, result) {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeySetIsEmpty(t *testing.T) {
|
||||
testCases := []struct {
|
||||
set KeySet
|
||||
expectedResult bool
|
||||
}{
|
||||
{NewKeySet(), true},
|
||||
{NewKeySet(S3Delimiter), false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.set.IsEmpty()
|
||||
|
||||
if testCase.expectedResult != result {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeySetString(t *testing.T) {
|
||||
testCases := []struct {
|
||||
set KeySet
|
||||
expectedResult string
|
||||
}{
|
||||
{NewKeySet(), `[]`},
|
||||
{NewKeySet(S3Delimiter), `[s3:delimiter]`},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.set.String()
|
||||
|
||||
if testCase.expectedResult != result {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeySetToSlice(t *testing.T) {
|
||||
testCases := []struct {
|
||||
set KeySet
|
||||
expectedResult []Key
|
||||
}{
|
||||
{NewKeySet(), []Key{}},
|
||||
{NewKeySet(S3Delimiter), []Key{S3Delimiter}},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.set.ToSlice()
|
||||
|
||||
if !reflect.DeepEqual(testCase.expectedResult, result) {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type name string
|
||||
|
||||
const (
|
||||
stringEquals name = "StringEquals"
|
||||
stringNotEquals = "StringNotEquals"
|
||||
stringEqualsIgnoreCase = "StringEqualsIgnoreCase"
|
||||
stringNotEqualsIgnoreCase = "StringNotEqualsIgnoreCase"
|
||||
stringLike = "StringLike"
|
||||
stringNotLike = "StringNotLike"
|
||||
binaryEquals = "BinaryEquals"
|
||||
ipAddress = "IpAddress"
|
||||
notIPAddress = "NotIpAddress"
|
||||
null = "Null"
|
||||
boolean = "Bool"
|
||||
)
|
||||
|
||||
var supportedConditions = []name{
|
||||
stringEquals,
|
||||
stringNotEquals,
|
||||
stringEqualsIgnoreCase,
|
||||
stringNotEqualsIgnoreCase,
|
||||
binaryEquals,
|
||||
stringLike,
|
||||
stringNotLike,
|
||||
ipAddress,
|
||||
notIPAddress,
|
||||
null,
|
||||
boolean,
|
||||
// Add new conditions here.
|
||||
}
|
||||
|
||||
// IsValid - checks if name is valid or not.
|
||||
func (n name) IsValid() bool {
|
||||
for _, supn := range supportedConditions {
|
||||
if n == supn {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON - encodes name to JSON data.
|
||||
func (n name) MarshalJSON() ([]byte, error) {
|
||||
if !n.IsValid() {
|
||||
return nil, fmt.Errorf("invalid name %v", n)
|
||||
}
|
||||
|
||||
return json.Marshal(string(n))
|
||||
}
|
||||
|
||||
// UnmarshalJSON - decodes JSON data to condition name.
|
||||
func (n *name) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsedName, err := parseName(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*n = parsedName
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseName(s string) (name, error) {
|
||||
n := name(s)
|
||||
|
||||
if n.IsValid() {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
return n, fmt.Errorf("invalid condition name '%v'", s)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNameIsValid(t *testing.T) {
|
||||
testCases := []struct {
|
||||
n name
|
||||
expectedResult bool
|
||||
}{
|
||||
{stringEquals, true},
|
||||
{stringNotEquals, true},
|
||||
{stringLike, true},
|
||||
{stringNotLike, true},
|
||||
{ipAddress, true},
|
||||
{notIPAddress, true},
|
||||
{null, true},
|
||||
{name("foo"), false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.n.IsValid()
|
||||
|
||||
if testCase.expectedResult != result {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNameMarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
n name
|
||||
expectedResult []byte
|
||||
expectErr bool
|
||||
}{
|
||||
{stringEquals, []byte(`"StringEquals"`), false},
|
||||
{stringNotEquals, []byte(`"StringNotEquals"`), false},
|
||||
{stringLike, []byte(`"StringLike"`), false},
|
||||
{stringNotLike, []byte(`"StringNotLike"`), false},
|
||||
{ipAddress, []byte(`"IpAddress"`), false},
|
||||
{notIPAddress, []byte(`"NotIpAddress"`), false},
|
||||
{null, []byte(`"Null"`), false},
|
||||
{name("foo"), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := json.Marshal(testCase.n)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if testCase.expectErr != expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNameUnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
data []byte
|
||||
expectedResult name
|
||||
expectErr bool
|
||||
}{
|
||||
{[]byte(`"StringEquals"`), stringEquals, false},
|
||||
{[]byte(`"foo"`), name(""), true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
var result name
|
||||
err := json.Unmarshal(testCase.data, &result)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if testCase.expectErr != expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if testCase.expectedResult != result {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// nullFunc - Null condition function. It checks whether Key is not present in given
|
||||
// values or not.
|
||||
// For example,
|
||||
// 1. if Key = S3XAmzCopySource and Value = true, at evaluate() it returns whether
|
||||
// S3XAmzCopySource is NOT in given value map or not.
|
||||
// 2. if Key = S3XAmzCopySource and Value = false, at evaluate() it returns whether
|
||||
// S3XAmzCopySource is in given value map or not.
|
||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html#Conditions_Null
|
||||
type nullFunc struct {
|
||||
k Key
|
||||
value bool
|
||||
}
|
||||
|
||||
// evaluate() - evaluates to check whether Key is present in given values or not.
|
||||
// Depending on condition boolean value, this function returns true or false.
|
||||
func (f nullFunc) evaluate(values map[string][]string) bool {
|
||||
requestValue, ok := values[http.CanonicalHeaderKey(f.k.Name())]
|
||||
if !ok {
|
||||
requestValue = values[f.k.Name()]
|
||||
}
|
||||
|
||||
if f.value {
|
||||
return len(requestValue) == 0
|
||||
}
|
||||
|
||||
return len(requestValue) != 0
|
||||
}
|
||||
|
||||
// key() - returns condition key which is used by this condition function.
|
||||
func (f nullFunc) key() Key {
|
||||
return f.k
|
||||
}
|
||||
|
||||
// name() - returns "Null" condition name.
|
||||
func (f nullFunc) name() name {
|
||||
return null
|
||||
}
|
||||
|
||||
func (f nullFunc) String() string {
|
||||
return fmt.Sprintf("%v:%v:%v", null, f.k, f.value)
|
||||
}
|
||||
|
||||
// toMap - returns map representation of this function.
|
||||
func (f nullFunc) toMap() map[Key]ValueSet {
|
||||
if !f.k.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[Key]ValueSet{
|
||||
f.k: NewValueSet(NewBoolValue(f.value)),
|
||||
}
|
||||
}
|
||||
|
||||
func newNullFunc(key Key, values ValueSet) (Function, error) {
|
||||
if len(values) != 1 {
|
||||
return nil, fmt.Errorf("only one value is allowed for Null condition")
|
||||
}
|
||||
|
||||
var value bool
|
||||
for v := range values {
|
||||
switch v.GetType() {
|
||||
case reflect.Bool:
|
||||
value, _ = v.GetBool()
|
||||
case reflect.String:
|
||||
var err error
|
||||
s, _ := v.GetString()
|
||||
if value, err = strconv.ParseBool(s); err != nil {
|
||||
return nil, fmt.Errorf("value must be a boolean string for Null condition")
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("value must be a boolean for Null condition")
|
||||
}
|
||||
}
|
||||
|
||||
return &nullFunc{key, value}, nil
|
||||
}
|
||||
|
||||
// NewNullFunc - returns new Null function.
|
||||
func NewNullFunc(key Key, value bool) (Function, error) {
|
||||
return &nullFunc{key, value}, nil
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNullFuncEvaluate(t *testing.T) {
|
||||
case1Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(true)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(false)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
values map[string][]string
|
||||
expectedResult bool
|
||||
}{
|
||||
{case1Function, map[string][]string{"prefix": {"true"}}, false},
|
||||
{case1Function, map[string][]string{"prefix": {"false"}}, false},
|
||||
{case1Function, map[string][]string{"prefix": {"mybucket/foo"}}, false},
|
||||
{case1Function, map[string][]string{}, true},
|
||||
{case1Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
{case2Function, map[string][]string{"prefix": {"true"}}, true},
|
||||
{case2Function, map[string][]string{"prefix": {"false"}}, true},
|
||||
{case2Function, map[string][]string{"prefix": {"mybucket/foo"}}, true},
|
||||
{case2Function, map[string][]string{}, false},
|
||||
{case2Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.evaluate(testCase.values)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Errorf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNullFuncKey(t *testing.T) {
|
||||
case1Function, err := newNullFunc(S3XAmzCopySource, NewValueSet(NewBoolValue(true)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
expectedResult Key
|
||||
}{
|
||||
{case1Function, S3XAmzCopySource},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.key()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNullFuncToMap(t *testing.T) {
|
||||
case1Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(true)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case1Result := map[Key]ValueSet{
|
||||
S3Prefix: NewValueSet(NewBoolValue(true)),
|
||||
}
|
||||
|
||||
case2Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(false)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Result := map[Key]ValueSet{
|
||||
S3Prefix: NewValueSet(NewBoolValue(false)),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
f Function
|
||||
expectedResult map[Key]ValueSet
|
||||
}{
|
||||
{case1Function, case1Result},
|
||||
{case2Function, case2Result},
|
||||
{&nullFunc{}, nil},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.f.toMap()
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewNullFunc(t *testing.T) {
|
||||
case1Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(true)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newNullFunc(S3Prefix, NewValueSet(NewBoolValue(false)))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
key Key
|
||||
values ValueSet
|
||||
expectedResult Function
|
||||
expectErr bool
|
||||
}{
|
||||
{S3Prefix, NewValueSet(NewBoolValue(true)), case1Function, false},
|
||||
{S3Prefix, NewValueSet(NewStringValue("false")), case2Function, false},
|
||||
// Multiple values error.
|
||||
{S3Prefix, NewValueSet(NewBoolValue(true), NewBoolValue(false)), nil, true},
|
||||
// Invalid boolean string error.
|
||||
{S3Prefix, NewValueSet(NewStringValue("foo")), nil, true},
|
||||
// Invalid value error.
|
||||
{S3Prefix, NewValueSet(NewIntValue(7)), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := newNullFunc(testCase.key, testCase.values)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
)
|
||||
|
||||
func toStringEqualsFuncString(n name, key Key, values set.StringSet) string {
|
||||
valueStrings := values.ToSlice()
|
||||
sort.Strings(valueStrings)
|
||||
|
||||
return fmt.Sprintf("%v:%v:%v", n, key, valueStrings)
|
||||
}
|
||||
|
||||
// stringEqualsFunc - String equals function. It checks whether value by Key in given
|
||||
// values map is in condition values.
|
||||
// For example,
|
||||
// - if values = ["mybucket/foo"], at evaluate() it returns whether string
|
||||
// in value map for Key is in values.
|
||||
type stringEqualsFunc struct {
|
||||
k Key
|
||||
values set.StringSet
|
||||
}
|
||||
|
||||
// evaluate() - evaluates to check whether value by Key in given values is in
|
||||
// condition values.
|
||||
func (f stringEqualsFunc) evaluate(values map[string][]string) bool {
|
||||
requestValue, ok := values[http.CanonicalHeaderKey(f.k.Name())]
|
||||
if !ok {
|
||||
requestValue = values[f.k.Name()]
|
||||
}
|
||||
|
||||
fvalues := f.values.ApplyFunc(substFuncFromValues(values))
|
||||
return !fvalues.Intersection(set.CreateStringSet(requestValue...)).IsEmpty()
|
||||
}
|
||||
|
||||
// key() - returns condition key which is used by this condition function.
|
||||
func (f stringEqualsFunc) key() Key {
|
||||
return f.k
|
||||
}
|
||||
|
||||
// name() - returns "StringEquals" condition name.
|
||||
func (f stringEqualsFunc) name() name {
|
||||
return stringEquals
|
||||
}
|
||||
|
||||
func (f stringEqualsFunc) String() string {
|
||||
return toStringEqualsFuncString(stringEquals, f.k, f.values)
|
||||
}
|
||||
|
||||
// toMap - returns map representation of this function.
|
||||
func (f stringEqualsFunc) toMap() map[Key]ValueSet {
|
||||
if !f.k.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
values := NewValueSet()
|
||||
for _, value := range f.values.ToSlice() {
|
||||
values.Add(NewStringValue(value))
|
||||
}
|
||||
|
||||
return map[Key]ValueSet{
|
||||
f.k: values,
|
||||
}
|
||||
}
|
||||
|
||||
// stringNotEqualsFunc - String not equals function. It checks whether value by Key in
|
||||
// given values is NOT in condition values.
|
||||
// For example,
|
||||
// - if values = ["mybucket/foo"], at evaluate() it returns whether string
|
||||
// in value map for Key is NOT in values.
|
||||
type stringNotEqualsFunc struct {
|
||||
stringEqualsFunc
|
||||
}
|
||||
|
||||
// evaluate() - evaluates to check whether value by Key in given values is NOT in
|
||||
// condition values.
|
||||
func (f stringNotEqualsFunc) evaluate(values map[string][]string) bool {
|
||||
return !f.stringEqualsFunc.evaluate(values)
|
||||
}
|
||||
|
||||
// name() - returns "StringNotEquals" condition name.
|
||||
func (f stringNotEqualsFunc) name() name {
|
||||
return stringNotEquals
|
||||
}
|
||||
|
||||
func (f stringNotEqualsFunc) String() string {
|
||||
return toStringEqualsFuncString(stringNotEquals, f.stringEqualsFunc.k, f.stringEqualsFunc.values)
|
||||
}
|
||||
|
||||
func valuesToStringSlice(n name, values ValueSet) ([]string, error) {
|
||||
valueStrings := []string{}
|
||||
|
||||
for value := range values {
|
||||
// FIXME: if AWS supports non-string values, we would need to support it.
|
||||
s, err := value.GetString()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("value must be a string for %v condition", n)
|
||||
}
|
||||
|
||||
valueStrings = append(valueStrings, s)
|
||||
}
|
||||
|
||||
return valueStrings, nil
|
||||
}
|
||||
|
||||
func validateStringEqualsValues(n name, key Key, values set.StringSet) error {
|
||||
for _, s := range values.ToSlice() {
|
||||
switch key {
|
||||
case S3XAmzCopySource:
|
||||
bucket, object := path2BucketAndObject(s)
|
||||
if object == "" {
|
||||
return fmt.Errorf("invalid value '%v' for '%v' for %v condition", s, S3XAmzCopySource, n)
|
||||
}
|
||||
if err := s3utils.CheckValidBucketName(bucket); err != nil {
|
||||
return err
|
||||
}
|
||||
case S3XAmzServerSideEncryption, S3XAmzServerSideEncryptionCustomerAlgorithm:
|
||||
if s != "AES256" {
|
||||
return fmt.Errorf("invalid value '%v' for '%v' for %v condition", s, S3XAmzServerSideEncryption, n)
|
||||
}
|
||||
case S3XAmzMetadataDirective:
|
||||
if s != "COPY" && s != "REPLACE" {
|
||||
return fmt.Errorf("invalid value '%v' for '%v' for %v condition", s, S3XAmzMetadataDirective, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// newStringEqualsFunc - returns new StringEquals function.
|
||||
func newStringEqualsFunc(key Key, values ValueSet) (Function, error) {
|
||||
valueStrings, err := valuesToStringSlice(stringEquals, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewStringEqualsFunc(key, valueStrings...)
|
||||
}
|
||||
|
||||
// NewStringEqualsFunc - returns new StringEquals function.
|
||||
func NewStringEqualsFunc(key Key, values ...string) (Function, error) {
|
||||
sset := set.CreateStringSet(values...)
|
||||
if err := validateStringEqualsValues(stringEquals, key, sset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &stringEqualsFunc{key, sset}, nil
|
||||
}
|
||||
|
||||
// newStringNotEqualsFunc - returns new StringNotEquals function.
|
||||
func newStringNotEqualsFunc(key Key, values ValueSet) (Function, error) {
|
||||
valueStrings, err := valuesToStringSlice(stringNotEquals, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewStringNotEqualsFunc(key, valueStrings...)
|
||||
}
|
||||
|
||||
// NewStringNotEqualsFunc - returns new StringNotEquals function.
|
||||
func NewStringNotEqualsFunc(key Key, values ...string) (Function, error) {
|
||||
sset := set.CreateStringSet(values...)
|
||||
if err := validateStringEqualsValues(stringNotEquals, key, sset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &stringNotEqualsFunc{stringEqualsFunc{key, sset}}, nil
|
||||
}
|
||||
@@ -0,0 +1,708 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStringEqualsFuncEvaluate(t *testing.T) {
|
||||
case1Function, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
values map[string][]string
|
||||
expectedResult bool
|
||||
}{
|
||||
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, true},
|
||||
{case1Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, false},
|
||||
{case1Function, map[string][]string{}, false},
|
||||
{case1Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
|
||||
{case2Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, true},
|
||||
{case2Function, map[string][]string{}, false},
|
||||
{case2Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
|
||||
{case3Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, true},
|
||||
{case3Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, false},
|
||||
{case3Function, map[string][]string{}, false},
|
||||
{case3Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
|
||||
{case4Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, true},
|
||||
{case4Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, false},
|
||||
{case4Function, map[string][]string{}, false},
|
||||
{case4Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.evaluate(testCase.values)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringEqualsFuncKey(t *testing.T) {
|
||||
case1Function, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
expectedResult Key
|
||||
}{
|
||||
{case1Function, S3XAmzCopySource},
|
||||
{case2Function, S3XAmzServerSideEncryption},
|
||||
{case3Function, S3XAmzMetadataDirective},
|
||||
{case4Function, S3LocationConstraint},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.key()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringEqualsFuncToMap(t *testing.T) {
|
||||
case1Function, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case1Result := map[Key]ValueSet{
|
||||
S3XAmzCopySource: NewValueSet(NewStringValue("mybucket/myobject")),
|
||||
}
|
||||
|
||||
case2Function, err := newStringEqualsFunc(S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/myobject"),
|
||||
NewStringValue("yourbucket/myobject"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Result := map[Key]ValueSet{
|
||||
S3XAmzCopySource: NewValueSet(
|
||||
NewStringValue("mybucket/myobject"),
|
||||
NewStringValue("yourbucket/myobject"),
|
||||
),
|
||||
}
|
||||
|
||||
case3Function, err := newStringEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Result := map[Key]ValueSet{
|
||||
S3XAmzServerSideEncryption: NewValueSet(NewStringValue("AES256")),
|
||||
}
|
||||
|
||||
case4Function, err := newStringEqualsFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES256"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Result := map[Key]ValueSet{
|
||||
S3XAmzServerSideEncryption: NewValueSet(
|
||||
NewStringValue("AES256"),
|
||||
),
|
||||
}
|
||||
|
||||
case5Function, err := newStringEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case5Result := map[Key]ValueSet{
|
||||
S3XAmzMetadataDirective: NewValueSet(NewStringValue("REPLACE")),
|
||||
}
|
||||
|
||||
case6Function, err := newStringEqualsFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPLACE"),
|
||||
NewStringValue("COPY"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case6Result := map[Key]ValueSet{
|
||||
S3XAmzMetadataDirective: NewValueSet(
|
||||
NewStringValue("REPLACE"),
|
||||
NewStringValue("COPY"),
|
||||
),
|
||||
}
|
||||
|
||||
case7Function, err := newStringEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case7Result := map[Key]ValueSet{
|
||||
S3LocationConstraint: NewValueSet(NewStringValue("eu-west-1")),
|
||||
}
|
||||
|
||||
case8Function, err := newStringEqualsFunc(S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-1"),
|
||||
NewStringValue("us-west-1"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case8Result := map[Key]ValueSet{
|
||||
S3LocationConstraint: NewValueSet(
|
||||
NewStringValue("eu-west-1"),
|
||||
NewStringValue("us-west-1"),
|
||||
),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
f Function
|
||||
expectedResult map[Key]ValueSet
|
||||
}{
|
||||
{case1Function, case1Result},
|
||||
{case2Function, case2Result},
|
||||
{case3Function, case3Result},
|
||||
{case4Function, case4Result},
|
||||
{case5Function, case5Result},
|
||||
{case6Function, case6Result},
|
||||
{case7Function, case7Result},
|
||||
{case8Function, case8Result},
|
||||
{&stringEqualsFunc{}, nil},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.f.toMap()
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringNotEqualsFuncEvaluate(t *testing.T) {
|
||||
case1Function, err := newStringNotEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringNotEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringNotEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
values map[string][]string
|
||||
expectedResult bool
|
||||
}{
|
||||
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, false},
|
||||
{case1Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, true},
|
||||
{case1Function, map[string][]string{}, true},
|
||||
{case1Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
|
||||
{case2Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, false},
|
||||
{case2Function, map[string][]string{}, true},
|
||||
{case2Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
|
||||
{case3Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, false},
|
||||
{case3Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, true},
|
||||
{case3Function, map[string][]string{}, true},
|
||||
{case3Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
|
||||
{case4Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, false},
|
||||
{case4Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, true},
|
||||
{case4Function, map[string][]string{}, true},
|
||||
{case4Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.evaluate(testCase.values)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringNotEqualsFuncKey(t *testing.T) {
|
||||
case1Function, err := newStringNotEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringNotEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringNotEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
expectedResult Key
|
||||
}{
|
||||
{case1Function, S3XAmzCopySource},
|
||||
{case2Function, S3XAmzServerSideEncryption},
|
||||
{case3Function, S3XAmzMetadataDirective},
|
||||
{case4Function, S3LocationConstraint},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.key()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringNotEqualsFuncToMap(t *testing.T) {
|
||||
case1Function, err := newStringNotEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case1Result := map[Key]ValueSet{
|
||||
S3XAmzCopySource: NewValueSet(NewStringValue("mybucket/myobject")),
|
||||
}
|
||||
|
||||
case2Function, err := newStringNotEqualsFunc(S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/myobject"),
|
||||
NewStringValue("yourbucket/myobject"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Result := map[Key]ValueSet{
|
||||
S3XAmzCopySource: NewValueSet(
|
||||
NewStringValue("mybucket/myobject"),
|
||||
NewStringValue("yourbucket/myobject"),
|
||||
),
|
||||
}
|
||||
|
||||
case3Function, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Result := map[Key]ValueSet{
|
||||
S3XAmzServerSideEncryption: NewValueSet(NewStringValue("AES256")),
|
||||
}
|
||||
|
||||
case4Function, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES256"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Result := map[Key]ValueSet{
|
||||
S3XAmzServerSideEncryption: NewValueSet(
|
||||
NewStringValue("AES256"),
|
||||
),
|
||||
}
|
||||
|
||||
case5Function, err := newStringNotEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case5Result := map[Key]ValueSet{
|
||||
S3XAmzMetadataDirective: NewValueSet(NewStringValue("REPLACE")),
|
||||
}
|
||||
|
||||
case6Function, err := newStringNotEqualsFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPLACE"),
|
||||
NewStringValue("COPY"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case6Result := map[Key]ValueSet{
|
||||
S3XAmzMetadataDirective: NewValueSet(
|
||||
NewStringValue("REPLACE"),
|
||||
NewStringValue("COPY"),
|
||||
),
|
||||
}
|
||||
|
||||
case7Function, err := newStringNotEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case7Result := map[Key]ValueSet{
|
||||
S3LocationConstraint: NewValueSet(NewStringValue("eu-west-1")),
|
||||
}
|
||||
|
||||
case8Function, err := newStringNotEqualsFunc(S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-1"),
|
||||
NewStringValue("us-west-1"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case8Result := map[Key]ValueSet{
|
||||
S3LocationConstraint: NewValueSet(
|
||||
NewStringValue("eu-west-1"),
|
||||
NewStringValue("us-west-1"),
|
||||
),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
f Function
|
||||
expectedResult map[Key]ValueSet
|
||||
}{
|
||||
{case1Function, case1Result},
|
||||
{case2Function, case2Result},
|
||||
{case3Function, case3Result},
|
||||
{case4Function, case4Result},
|
||||
{case5Function, case5Result},
|
||||
{case6Function, case6Result},
|
||||
{case7Function, case7Result},
|
||||
{case8Function, case8Result},
|
||||
{&stringNotEqualsFunc{}, nil},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.f.toMap()
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewStringEqualsFunc(t *testing.T) {
|
||||
case1Function, err := newStringEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringEqualsFunc(S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/myobject"),
|
||||
NewStringValue("yourbucket/myobject"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringEqualsFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES256"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case5Function, err := newStringEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case6Function, err := newStringEqualsFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPLACE"),
|
||||
NewStringValue("COPY"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case7Function, err := newStringEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case8Function, err := newStringEqualsFunc(S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-1"),
|
||||
NewStringValue("us-west-1"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
key Key
|
||||
values ValueSet
|
||||
expectedResult Function
|
||||
expectErr bool
|
||||
}{
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")), case1Function, false},
|
||||
{S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/myobject"),
|
||||
NewStringValue("yourbucket/myobject"),
|
||||
), case2Function, false},
|
||||
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")), case3Function, false},
|
||||
{S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES256"),
|
||||
), case4Function, false},
|
||||
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")), case5Function, false},
|
||||
{S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPLACE"),
|
||||
NewStringValue("COPY"),
|
||||
), case6Function, false},
|
||||
|
||||
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")), case7Function, false},
|
||||
{S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-1"),
|
||||
NewStringValue("us-west-1"),
|
||||
), case8Function, false},
|
||||
|
||||
// Unsupported value error.
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"), NewIntValue(7)), nil, true},
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"), NewIntValue(7)), nil, true},
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"), NewIntValue(7)), nil, true},
|
||||
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"), NewIntValue(7)), nil, true},
|
||||
|
||||
// Invalid value error.
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket")), nil, true},
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("SSE-C")), nil, true},
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("DUPLICATE")), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := newStringEqualsFunc(testCase.key, testCase.values)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewStringNotEqualsFunc(t *testing.T) {
|
||||
case1Function, err := newStringNotEqualsFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringNotEqualsFunc(S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/myobject"),
|
||||
NewStringValue("yourbucket/myobject"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringNotEqualsFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES256"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case5Function, err := newStringNotEqualsFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case6Function, err := newStringNotEqualsFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPLACE"),
|
||||
NewStringValue("COPY"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case7Function, err := newStringNotEqualsFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case8Function, err := newStringNotEqualsFunc(S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-1"),
|
||||
NewStringValue("us-west-1"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
key Key
|
||||
values ValueSet
|
||||
expectedResult Function
|
||||
expectErr bool
|
||||
}{
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")), case1Function, false},
|
||||
{S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/myobject"),
|
||||
NewStringValue("yourbucket/myobject"),
|
||||
), case2Function, false},
|
||||
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")), case3Function, false},
|
||||
{S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES256"),
|
||||
), case4Function, false},
|
||||
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")), case5Function, false},
|
||||
{S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPLACE"),
|
||||
NewStringValue("COPY"),
|
||||
), case6Function, false},
|
||||
|
||||
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")), case7Function, false},
|
||||
{S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-1"),
|
||||
NewStringValue("us-west-1"),
|
||||
), case8Function, false},
|
||||
|
||||
// Unsupported value error.
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"), NewIntValue(7)), nil, true},
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"), NewIntValue(7)), nil, true},
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"), NewIntValue(7)), nil, true},
|
||||
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"), NewIntValue(7)), nil, true},
|
||||
|
||||
// Invalid value error.
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket")), nil, true},
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("SSE-C")), nil, true},
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("DUPLICATE")), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := newStringNotEqualsFunc(testCase.key, testCase.values)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
)
|
||||
|
||||
func toStringEqualsIgnoreCaseFuncString(n name, key Key, values set.StringSet) string {
|
||||
valueStrings := values.ToSlice()
|
||||
sort.Strings(valueStrings)
|
||||
|
||||
return fmt.Sprintf("%v:%v:%v", n, key, valueStrings)
|
||||
}
|
||||
|
||||
// stringEqualsIgnoreCaseFunc - String equals function. It checks whether value by Key in given
|
||||
// values map is in condition values.
|
||||
// For example,
|
||||
// - if values = ["mybucket/foo"], at evaluate() it returns whether string
|
||||
// in value map for Key is in values.
|
||||
type stringEqualsIgnoreCaseFunc struct {
|
||||
k Key
|
||||
values set.StringSet
|
||||
}
|
||||
|
||||
// evaluate() - evaluates to check whether value by Key in given values is in
|
||||
// condition values, ignores case.
|
||||
func (f stringEqualsIgnoreCaseFunc) evaluate(values map[string][]string) bool {
|
||||
requestValue, ok := values[http.CanonicalHeaderKey(f.k.Name())]
|
||||
if !ok {
|
||||
requestValue = values[f.k.Name()]
|
||||
}
|
||||
|
||||
fvalues := f.values.ApplyFunc(substFuncFromValues(values))
|
||||
|
||||
for _, v := range requestValue {
|
||||
if !fvalues.FuncMatch(strings.EqualFold, v).IsEmpty() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// key() - returns condition key which is used by this condition function.
|
||||
func (f stringEqualsIgnoreCaseFunc) key() Key {
|
||||
return f.k
|
||||
}
|
||||
|
||||
// name() - returns "StringEqualsIgnoreCase" condition name.
|
||||
func (f stringEqualsIgnoreCaseFunc) name() name {
|
||||
return stringEqualsIgnoreCase
|
||||
}
|
||||
|
||||
func (f stringEqualsIgnoreCaseFunc) String() string {
|
||||
return toStringEqualsIgnoreCaseFuncString(stringEqualsIgnoreCase, f.k, f.values)
|
||||
}
|
||||
|
||||
// toMap - returns map representation of this function.
|
||||
func (f stringEqualsIgnoreCaseFunc) toMap() map[Key]ValueSet {
|
||||
if !f.k.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
values := NewValueSet()
|
||||
for _, value := range f.values.ToSlice() {
|
||||
values.Add(NewStringValue(value))
|
||||
}
|
||||
|
||||
return map[Key]ValueSet{
|
||||
f.k: values,
|
||||
}
|
||||
}
|
||||
|
||||
// stringNotEqualsIgnoreCaseFunc - String not equals function. It checks whether value by Key in
|
||||
// given values is NOT in condition values.
|
||||
// For example,
|
||||
// - if values = ["mybucket/foo"], at evaluate() it returns whether string
|
||||
// in value map for Key is NOT in values.
|
||||
type stringNotEqualsIgnoreCaseFunc struct {
|
||||
stringEqualsIgnoreCaseFunc
|
||||
}
|
||||
|
||||
// evaluate() - evaluates to check whether value by Key in given values is NOT in
|
||||
// condition values.
|
||||
func (f stringNotEqualsIgnoreCaseFunc) evaluate(values map[string][]string) bool {
|
||||
return !f.stringEqualsIgnoreCaseFunc.evaluate(values)
|
||||
}
|
||||
|
||||
// name() - returns "StringNotEqualsIgnoreCase" condition name.
|
||||
func (f stringNotEqualsIgnoreCaseFunc) name() name {
|
||||
return stringNotEqualsIgnoreCase
|
||||
}
|
||||
|
||||
func (f stringNotEqualsIgnoreCaseFunc) String() string {
|
||||
return toStringEqualsIgnoreCaseFuncString(stringNotEqualsIgnoreCase, f.stringEqualsIgnoreCaseFunc.k, f.stringEqualsIgnoreCaseFunc.values)
|
||||
}
|
||||
|
||||
func validateStringEqualsIgnoreCaseValues(n name, key Key, values set.StringSet) error {
|
||||
return validateStringEqualsValues(n, key, values)
|
||||
}
|
||||
|
||||
// newStringEqualsIgnoreCaseFunc - returns new StringEqualsIgnoreCase function.
|
||||
func newStringEqualsIgnoreCaseFunc(key Key, values ValueSet) (Function, error) {
|
||||
valueStrings, err := valuesToStringSlice(stringEqualsIgnoreCase, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewStringEqualsIgnoreCaseFunc(key, valueStrings...)
|
||||
}
|
||||
|
||||
// NewStringEqualsIgnoreCaseFunc - returns new StringEqualsIgnoreCase function.
|
||||
func NewStringEqualsIgnoreCaseFunc(key Key, values ...string) (Function, error) {
|
||||
sset := set.CreateStringSet(values...)
|
||||
if err := validateStringEqualsIgnoreCaseValues(stringEqualsIgnoreCase, key, sset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &stringEqualsIgnoreCaseFunc{key, sset}, nil
|
||||
}
|
||||
|
||||
// newStringNotEqualsIgnoreCaseFunc - returns new StringNotEqualsIgnoreCase function.
|
||||
func newStringNotEqualsIgnoreCaseFunc(key Key, values ValueSet) (Function, error) {
|
||||
valueStrings, err := valuesToStringSlice(stringNotEqualsIgnoreCase, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewStringNotEqualsIgnoreCaseFunc(key, valueStrings...)
|
||||
}
|
||||
|
||||
// NewStringNotEqualsIgnoreCaseFunc - returns new StringNotEqualsIgnoreCase function.
|
||||
func NewStringNotEqualsIgnoreCaseFunc(key Key, values ...string) (Function, error) {
|
||||
sset := set.CreateStringSet(values...)
|
||||
if err := validateStringEqualsIgnoreCaseValues(stringNotEqualsIgnoreCase, key, sset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &stringNotEqualsIgnoreCaseFunc{stringEqualsIgnoreCaseFunc{key, sset}}, nil
|
||||
}
|
||||
@@ -0,0 +1,710 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStringEqualsIgnoreCaseFuncEvaluate(t *testing.T) {
|
||||
case1Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringEqualsIgnoreCaseFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
values map[string][]string
|
||||
expectedResult bool
|
||||
}{
|
||||
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, true},
|
||||
{case1Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, false},
|
||||
{case1Function, map[string][]string{}, false},
|
||||
{case1Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
|
||||
{case2Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, true},
|
||||
{case2Function, map[string][]string{"x-amz-server-side-encryption": {"aes256"}}, true},
|
||||
{case2Function, map[string][]string{}, false},
|
||||
{case2Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
|
||||
{case3Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, true},
|
||||
{case3Function, map[string][]string{"x-amz-metadata-directive": {"replace"}}, true},
|
||||
{case3Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, false},
|
||||
{case3Function, map[string][]string{}, false},
|
||||
{case3Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
|
||||
{case4Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, true},
|
||||
{case4Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, false},
|
||||
{case4Function, map[string][]string{}, false},
|
||||
{case4Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.evaluate(testCase.values)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringEqualsIgnoreCaseFuncKey(t *testing.T) {
|
||||
case1Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringEqualsIgnoreCaseFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
expectedResult Key
|
||||
}{
|
||||
{case1Function, S3XAmzCopySource},
|
||||
{case2Function, S3XAmzServerSideEncryption},
|
||||
{case3Function, S3XAmzMetadataDirective},
|
||||
{case4Function, S3LocationConstraint},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.key()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringEqualsIgnoreCaseFuncToMap(t *testing.T) {
|
||||
case1Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case1Result := map[Key]ValueSet{
|
||||
S3XAmzCopySource: NewValueSet(NewStringValue("mybucket/myobject")),
|
||||
}
|
||||
|
||||
case2Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/myobject"),
|
||||
NewStringValue("yourbucket/myobject"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Result := map[Key]ValueSet{
|
||||
S3XAmzCopySource: NewValueSet(
|
||||
NewStringValue("mybucket/myobject"),
|
||||
NewStringValue("yourbucket/myobject"),
|
||||
),
|
||||
}
|
||||
|
||||
case3Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Result := map[Key]ValueSet{
|
||||
S3XAmzServerSideEncryption: NewValueSet(NewStringValue("AES256")),
|
||||
}
|
||||
|
||||
case4Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES256"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Result := map[Key]ValueSet{
|
||||
S3XAmzServerSideEncryption: NewValueSet(
|
||||
NewStringValue("AES256"),
|
||||
),
|
||||
}
|
||||
|
||||
case5Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case5Result := map[Key]ValueSet{
|
||||
S3XAmzMetadataDirective: NewValueSet(NewStringValue("REPLACE")),
|
||||
}
|
||||
|
||||
case6Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPLACE"),
|
||||
NewStringValue("COPY"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case6Result := map[Key]ValueSet{
|
||||
S3XAmzMetadataDirective: NewValueSet(
|
||||
NewStringValue("REPLACE"),
|
||||
NewStringValue("COPY"),
|
||||
),
|
||||
}
|
||||
|
||||
case7Function, err := newStringEqualsIgnoreCaseFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case7Result := map[Key]ValueSet{
|
||||
S3LocationConstraint: NewValueSet(NewStringValue("eu-west-1")),
|
||||
}
|
||||
|
||||
case8Function, err := newStringEqualsIgnoreCaseFunc(S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-1"),
|
||||
NewStringValue("us-west-1"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case8Result := map[Key]ValueSet{
|
||||
S3LocationConstraint: NewValueSet(
|
||||
NewStringValue("eu-west-1"),
|
||||
NewStringValue("us-west-1"),
|
||||
),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
f Function
|
||||
expectedResult map[Key]ValueSet
|
||||
}{
|
||||
{case1Function, case1Result},
|
||||
{case2Function, case2Result},
|
||||
{case3Function, case3Result},
|
||||
{case4Function, case4Result},
|
||||
{case5Function, case5Result},
|
||||
{case6Function, case6Result},
|
||||
{case7Function, case7Result},
|
||||
{case8Function, case8Result},
|
||||
{&stringEqualsFunc{}, nil},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.f.toMap()
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringNotEqualsIgnoreCaseFuncEvaluate(t *testing.T) {
|
||||
case1Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringNotEqualsIgnoreCaseFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
values map[string][]string
|
||||
expectedResult bool
|
||||
}{
|
||||
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, false},
|
||||
{case1Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, true},
|
||||
{case1Function, map[string][]string{}, true},
|
||||
{case1Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
|
||||
{case2Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, false},
|
||||
{case2Function, map[string][]string{}, true},
|
||||
{case2Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
|
||||
{case3Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, false},
|
||||
{case3Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, true},
|
||||
{case3Function, map[string][]string{}, true},
|
||||
{case3Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
|
||||
{case4Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, false},
|
||||
{case4Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, true},
|
||||
{case4Function, map[string][]string{}, true},
|
||||
{case4Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.evaluate(testCase.values)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringNotEqualsIgnoreCaseFuncKey(t *testing.T) {
|
||||
case1Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringNotEqualsIgnoreCaseFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
expectedResult Key
|
||||
}{
|
||||
{case1Function, S3XAmzCopySource},
|
||||
{case2Function, S3XAmzServerSideEncryption},
|
||||
{case3Function, S3XAmzMetadataDirective},
|
||||
{case4Function, S3LocationConstraint},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.key()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringNotEqualsIgnoreCaseFuncToMap(t *testing.T) {
|
||||
case1Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case1Result := map[Key]ValueSet{
|
||||
S3XAmzCopySource: NewValueSet(NewStringValue("mybucket/myobject")),
|
||||
}
|
||||
|
||||
case2Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/myobject"),
|
||||
NewStringValue("yourbucket/myobject"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Result := map[Key]ValueSet{
|
||||
S3XAmzCopySource: NewValueSet(
|
||||
NewStringValue("mybucket/myobject"),
|
||||
NewStringValue("yourbucket/myobject"),
|
||||
),
|
||||
}
|
||||
|
||||
case3Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Result := map[Key]ValueSet{
|
||||
S3XAmzServerSideEncryption: NewValueSet(NewStringValue("AES256")),
|
||||
}
|
||||
|
||||
case4Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES256"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Result := map[Key]ValueSet{
|
||||
S3XAmzServerSideEncryption: NewValueSet(
|
||||
NewStringValue("AES256"),
|
||||
),
|
||||
}
|
||||
|
||||
case5Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case5Result := map[Key]ValueSet{
|
||||
S3XAmzMetadataDirective: NewValueSet(NewStringValue("REPLACE")),
|
||||
}
|
||||
|
||||
case6Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPLACE"),
|
||||
NewStringValue("COPY"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case6Result := map[Key]ValueSet{
|
||||
S3XAmzMetadataDirective: NewValueSet(
|
||||
NewStringValue("REPLACE"),
|
||||
NewStringValue("COPY"),
|
||||
),
|
||||
}
|
||||
|
||||
case7Function, err := newStringNotEqualsIgnoreCaseFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case7Result := map[Key]ValueSet{
|
||||
S3LocationConstraint: NewValueSet(NewStringValue("eu-west-1")),
|
||||
}
|
||||
|
||||
case8Function, err := newStringNotEqualsIgnoreCaseFunc(S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-1"),
|
||||
NewStringValue("us-west-1"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case8Result := map[Key]ValueSet{
|
||||
S3LocationConstraint: NewValueSet(
|
||||
NewStringValue("eu-west-1"),
|
||||
NewStringValue("us-west-1"),
|
||||
),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
f Function
|
||||
expectedResult map[Key]ValueSet
|
||||
}{
|
||||
{case1Function, case1Result},
|
||||
{case2Function, case2Result},
|
||||
{case3Function, case3Result},
|
||||
{case4Function, case4Result},
|
||||
{case5Function, case5Result},
|
||||
{case6Function, case6Result},
|
||||
{case7Function, case7Result},
|
||||
{case8Function, case8Result},
|
||||
{&stringNotEqualsFunc{}, nil},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.f.toMap()
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewStringEqualsIgnoreCaseFunc(t *testing.T) {
|
||||
case1Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/myobject"),
|
||||
NewStringValue("yourbucket/myobject"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES256"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case5Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case6Function, err := newStringEqualsIgnoreCaseFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPLACE"),
|
||||
NewStringValue("COPY"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case7Function, err := newStringEqualsIgnoreCaseFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case8Function, err := newStringEqualsIgnoreCaseFunc(S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-1"),
|
||||
NewStringValue("us-west-1"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
key Key
|
||||
values ValueSet
|
||||
expectedResult Function
|
||||
expectErr bool
|
||||
}{
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")), case1Function, false},
|
||||
{S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/myobject"),
|
||||
NewStringValue("yourbucket/myobject"),
|
||||
), case2Function, false},
|
||||
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")), case3Function, false},
|
||||
{S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES256"),
|
||||
), case4Function, false},
|
||||
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")), case5Function, false},
|
||||
{S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPLACE"),
|
||||
NewStringValue("COPY"),
|
||||
), case6Function, false},
|
||||
|
||||
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")), case7Function, false},
|
||||
{S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-1"),
|
||||
NewStringValue("us-west-1"),
|
||||
), case8Function, false},
|
||||
|
||||
// Unsupported value error.
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"), NewIntValue(7)), nil, true},
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"), NewIntValue(7)), nil, true},
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"), NewIntValue(7)), nil, true},
|
||||
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"), NewIntValue(7)), nil, true},
|
||||
|
||||
// Invalid value error.
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket")), nil, true},
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("SSE-C")), nil, true},
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("DUPLICATE")), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := newStringEqualsIgnoreCaseFunc(testCase.key, testCase.values)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewStringNotEqualsIgnoreCaseFunc(t *testing.T) {
|
||||
case1Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/myobject"),
|
||||
NewStringValue("yourbucket/myobject"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES256"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case5Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case6Function, err := newStringNotEqualsIgnoreCaseFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPLACE"),
|
||||
NewStringValue("COPY"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case7Function, err := newStringNotEqualsIgnoreCaseFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case8Function, err := newStringNotEqualsIgnoreCaseFunc(S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-1"),
|
||||
NewStringValue("us-west-1"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
key Key
|
||||
values ValueSet
|
||||
expectedResult Function
|
||||
expectErr bool
|
||||
}{
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")), case1Function, false},
|
||||
{S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/myobject"),
|
||||
NewStringValue("yourbucket/myobject"),
|
||||
), case2Function, false},
|
||||
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")), case3Function, false},
|
||||
{S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES256"),
|
||||
), case4Function, false},
|
||||
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")), case5Function, false},
|
||||
{S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPLACE"),
|
||||
NewStringValue("COPY"),
|
||||
), case6Function, false},
|
||||
|
||||
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")), case7Function, false},
|
||||
{S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-1"),
|
||||
NewStringValue("us-west-1"),
|
||||
), case8Function, false},
|
||||
|
||||
// Unsupported value error.
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"), NewIntValue(7)), nil, true},
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"), NewIntValue(7)), nil, true},
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"), NewIntValue(7)), nil, true},
|
||||
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"), NewIntValue(7)), nil, true},
|
||||
|
||||
// Invalid value error.
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket")), nil, true},
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("SSE-C")), nil, true},
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("DUPLICATE")), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := newStringNotEqualsIgnoreCaseFunc(testCase.key, testCase.values)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/pkg/wildcard"
|
||||
)
|
||||
|
||||
func toStringLikeFuncString(n name, key Key, values set.StringSet) string {
|
||||
valueStrings := values.ToSlice()
|
||||
sort.Strings(valueStrings)
|
||||
|
||||
return fmt.Sprintf("%v:%v:%v", n, key, valueStrings)
|
||||
}
|
||||
|
||||
// stringLikeFunc - String like function. It checks whether value by Key in given
|
||||
// values map is widcard matching in condition values.
|
||||
// For example,
|
||||
// - if values = ["mybucket/foo*"], at evaluate() it returns whether string
|
||||
// in value map for Key is wildcard matching in values.
|
||||
type stringLikeFunc struct {
|
||||
k Key
|
||||
values set.StringSet
|
||||
}
|
||||
|
||||
// evaluate() - evaluates to check whether value by Key in given values is wildcard
|
||||
// matching in condition values.
|
||||
func (f stringLikeFunc) evaluate(values map[string][]string) bool {
|
||||
requestValue, ok := values[http.CanonicalHeaderKey(f.k.Name())]
|
||||
if !ok {
|
||||
requestValue = values[f.k.Name()]
|
||||
}
|
||||
|
||||
fvalues := f.values.ApplyFunc(substFuncFromValues(values))
|
||||
|
||||
for _, v := range requestValue {
|
||||
if !fvalues.FuncMatch(wildcard.Match, v).IsEmpty() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// key() - returns condition key which is used by this condition function.
|
||||
func (f stringLikeFunc) key() Key {
|
||||
return f.k
|
||||
}
|
||||
|
||||
// name() - returns "StringLike" function name.
|
||||
func (f stringLikeFunc) name() name {
|
||||
return stringLike
|
||||
}
|
||||
|
||||
func (f stringLikeFunc) String() string {
|
||||
return toStringLikeFuncString(stringLike, f.k, f.values)
|
||||
}
|
||||
|
||||
// toMap - returns map representation of this function.
|
||||
func (f stringLikeFunc) toMap() map[Key]ValueSet {
|
||||
if !f.k.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
values := NewValueSet()
|
||||
for _, value := range f.values.ToSlice() {
|
||||
values.Add(NewStringValue(value))
|
||||
}
|
||||
|
||||
return map[Key]ValueSet{
|
||||
f.k: values,
|
||||
}
|
||||
}
|
||||
|
||||
// stringNotLikeFunc - String not like function. It checks whether value by Key in given
|
||||
// values map is NOT widcard matching in condition values.
|
||||
// For example,
|
||||
// - if values = ["mybucket/foo*"], at evaluate() it returns whether string
|
||||
// in value map for Key is NOT wildcard matching in values.
|
||||
type stringNotLikeFunc struct {
|
||||
stringLikeFunc
|
||||
}
|
||||
|
||||
// evaluate() - evaluates to check whether value by Key in given values is NOT wildcard
|
||||
// matching in condition values.
|
||||
func (f stringNotLikeFunc) evaluate(values map[string][]string) bool {
|
||||
return !f.stringLikeFunc.evaluate(values)
|
||||
}
|
||||
|
||||
// name() - returns "StringNotLike" function name.
|
||||
func (f stringNotLikeFunc) name() name {
|
||||
return stringNotLike
|
||||
}
|
||||
|
||||
func (f stringNotLikeFunc) String() string {
|
||||
return toStringLikeFuncString(stringNotLike, f.stringLikeFunc.k, f.stringLikeFunc.values)
|
||||
}
|
||||
|
||||
func validateStringLikeValues(n name, key Key, values set.StringSet) error {
|
||||
for _, s := range values.ToSlice() {
|
||||
switch key {
|
||||
case S3XAmzCopySource:
|
||||
bucket, object := path2BucketAndObject(s)
|
||||
if object == "" {
|
||||
return fmt.Errorf("invalid value '%v' for '%v' for %v condition", s, S3XAmzCopySource, n)
|
||||
}
|
||||
if err := s3utils.CheckValidBucketName(bucket); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// newStringLikeFunc - returns new StringLike function.
|
||||
func newStringLikeFunc(key Key, values ValueSet) (Function, error) {
|
||||
valueStrings, err := valuesToStringSlice(stringLike, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewStringLikeFunc(key, valueStrings...)
|
||||
}
|
||||
|
||||
// NewStringLikeFunc - returns new StringLike function.
|
||||
func NewStringLikeFunc(key Key, values ...string) (Function, error) {
|
||||
sset := set.CreateStringSet(values...)
|
||||
if err := validateStringLikeValues(stringLike, key, sset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &stringLikeFunc{key, sset}, nil
|
||||
}
|
||||
|
||||
// newStringNotLikeFunc - returns new StringNotLike function.
|
||||
func newStringNotLikeFunc(key Key, values ValueSet) (Function, error) {
|
||||
valueStrings, err := valuesToStringSlice(stringNotLike, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewStringNotLikeFunc(key, valueStrings...)
|
||||
}
|
||||
|
||||
// NewStringNotLikeFunc - returns new StringNotLike function.
|
||||
func NewStringNotLikeFunc(key Key, values ...string) (Function, error) {
|
||||
sset := set.CreateStringSet(values...)
|
||||
if err := validateStringLikeValues(stringNotLike, key, sset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &stringNotLikeFunc{stringLikeFunc{key, sset}}, nil
|
||||
}
|
||||
@@ -0,0 +1,798 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStringLikeFuncEvaluate(t *testing.T) {
|
||||
case1Function, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case5Function, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case6Function, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case7Function, err := newStringLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case8Function, err := newStringLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
values map[string][]string
|
||||
expectedResult bool
|
||||
}{
|
||||
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, true},
|
||||
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject.png"}}, true},
|
||||
{case1Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, false},
|
||||
{case1Function, map[string][]string{}, false},
|
||||
{case1Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
|
||||
{case2Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, true},
|
||||
{case2Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject.png"}}, false},
|
||||
{case2Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, false},
|
||||
{case2Function, map[string][]string{}, false},
|
||||
{case2Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
|
||||
{case3Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, true},
|
||||
{case3Function, map[string][]string{"x-amz-server-side-encryption": {"AES512"}}, true},
|
||||
{case3Function, map[string][]string{}, false},
|
||||
{case3Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
|
||||
{case4Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, true},
|
||||
{case4Function, map[string][]string{"x-amz-server-side-encryption": {"AES512"}}, false},
|
||||
{case4Function, map[string][]string{}, false},
|
||||
{case4Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
|
||||
{case5Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, true},
|
||||
{case5Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE/COPY"}}, true},
|
||||
{case5Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, false},
|
||||
{case5Function, map[string][]string{}, false},
|
||||
{case5Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
|
||||
{case6Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, true},
|
||||
{case6Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE/COPY"}}, false},
|
||||
{case6Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, false},
|
||||
{case6Function, map[string][]string{}, false},
|
||||
{case6Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
|
||||
{case7Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, true},
|
||||
{case7Function, map[string][]string{"LocationConstraint": {"eu-west-2"}}, true},
|
||||
{case7Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, false},
|
||||
{case7Function, map[string][]string{}, false},
|
||||
{case7Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
|
||||
{case8Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, true},
|
||||
{case8Function, map[string][]string{"LocationConstraint": {"eu-west-2"}}, false},
|
||||
{case8Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, false},
|
||||
{case8Function, map[string][]string{}, false},
|
||||
{case8Function, map[string][]string{"delimiter": {"/"}}, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.evaluate(testCase.values)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringLikeFuncKey(t *testing.T) {
|
||||
case1Function, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
expectedResult Key
|
||||
}{
|
||||
{case1Function, S3XAmzCopySource},
|
||||
{case2Function, S3XAmzServerSideEncryption},
|
||||
{case3Function, S3XAmzMetadataDirective},
|
||||
{case4Function, S3LocationConstraint},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.key()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringLikeFuncToMap(t *testing.T) {
|
||||
case1Function, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case1Result := map[Key]ValueSet{
|
||||
S3XAmzCopySource: NewValueSet(NewStringValue("mybucket/*")),
|
||||
}
|
||||
|
||||
case2Function, err := newStringLikeFunc(S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/*"),
|
||||
NewStringValue("yourbucket/myobject*"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Result := map[Key]ValueSet{
|
||||
S3XAmzCopySource: NewValueSet(
|
||||
NewStringValue("mybucket/*"),
|
||||
NewStringValue("yourbucket/myobject*"),
|
||||
),
|
||||
}
|
||||
|
||||
case3Function, err := newStringLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Result := map[Key]ValueSet{
|
||||
S3XAmzServerSideEncryption: NewValueSet(NewStringValue("AES*")),
|
||||
}
|
||||
|
||||
case4Function, err := newStringLikeFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES*"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Result := map[Key]ValueSet{
|
||||
S3XAmzServerSideEncryption: NewValueSet(
|
||||
NewStringValue("AES*"),
|
||||
),
|
||||
}
|
||||
|
||||
case5Function, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case5Result := map[Key]ValueSet{
|
||||
S3XAmzMetadataDirective: NewValueSet(NewStringValue("REPL*")),
|
||||
}
|
||||
|
||||
case6Function, err := newStringLikeFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPL*"),
|
||||
NewStringValue("COPY*"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case6Result := map[Key]ValueSet{
|
||||
S3XAmzMetadataDirective: NewValueSet(
|
||||
NewStringValue("REPL*"),
|
||||
NewStringValue("COPY*"),
|
||||
),
|
||||
}
|
||||
|
||||
case7Function, err := newStringLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case7Result := map[Key]ValueSet{
|
||||
S3LocationConstraint: NewValueSet(NewStringValue("eu-west-*")),
|
||||
}
|
||||
|
||||
case8Function, err := newStringLikeFunc(S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-*"),
|
||||
NewStringValue("us-west-*"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case8Result := map[Key]ValueSet{
|
||||
S3LocationConstraint: NewValueSet(
|
||||
NewStringValue("eu-west-*"),
|
||||
NewStringValue("us-west-*"),
|
||||
),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
f Function
|
||||
expectedResult map[Key]ValueSet
|
||||
}{
|
||||
{case1Function, case1Result},
|
||||
{case2Function, case2Result},
|
||||
{case3Function, case3Result},
|
||||
{case4Function, case4Result},
|
||||
{case5Function, case5Result},
|
||||
{case6Function, case6Result},
|
||||
{case7Function, case7Result},
|
||||
{case8Function, case8Result},
|
||||
{&stringLikeFunc{}, nil},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.f.toMap()
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringNotLikeFuncEvaluate(t *testing.T) {
|
||||
case1Function, err := newStringNotLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringNotLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case5Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case6Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case7Function, err := newStringNotLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case8Function, err := newStringNotLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
values map[string][]string
|
||||
expectedResult bool
|
||||
}{
|
||||
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, false},
|
||||
{case1Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject.png"}}, false},
|
||||
{case1Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, true},
|
||||
{case1Function, map[string][]string{}, true},
|
||||
{case1Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
|
||||
{case2Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject"}}, false},
|
||||
{case2Function, map[string][]string{"x-amz-copy-source": {"mybucket/myobject.png"}}, true},
|
||||
{case2Function, map[string][]string{"x-amz-copy-source": {"yourbucket/myobject"}}, true},
|
||||
{case2Function, map[string][]string{}, true},
|
||||
{case2Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
|
||||
{case3Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, false},
|
||||
{case3Function, map[string][]string{"x-amz-server-side-encryption": {"AES512"}}, false},
|
||||
{case3Function, map[string][]string{}, true},
|
||||
{case3Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
|
||||
{case4Function, map[string][]string{"x-amz-server-side-encryption": {"AES256"}}, false},
|
||||
{case4Function, map[string][]string{"x-amz-server-side-encryption": {"AES512"}}, true},
|
||||
{case4Function, map[string][]string{}, true},
|
||||
{case4Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
|
||||
{case5Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, false},
|
||||
{case5Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE/COPY"}}, false},
|
||||
{case5Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, true},
|
||||
{case5Function, map[string][]string{}, true},
|
||||
{case5Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
|
||||
{case6Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE"}}, false},
|
||||
{case6Function, map[string][]string{"x-amz-metadata-directive": {"REPLACE/COPY"}}, true},
|
||||
{case6Function, map[string][]string{"x-amz-metadata-directive": {"COPY"}}, true},
|
||||
{case6Function, map[string][]string{}, true},
|
||||
{case6Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
|
||||
{case7Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, false},
|
||||
{case7Function, map[string][]string{"LocationConstraint": {"eu-west-2"}}, false},
|
||||
{case7Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, true},
|
||||
{case7Function, map[string][]string{}, true},
|
||||
{case7Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
|
||||
{case8Function, map[string][]string{"LocationConstraint": {"eu-west-1"}}, false},
|
||||
{case8Function, map[string][]string{"LocationConstraint": {"eu-west-2"}}, true},
|
||||
{case8Function, map[string][]string{"LocationConstraint": {"us-east-1"}}, true},
|
||||
{case8Function, map[string][]string{}, true},
|
||||
{case8Function, map[string][]string{"delimiter": {"/"}}, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.evaluate(testCase.values)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringNotLikeFuncKey(t *testing.T) {
|
||||
case1Function, err := newStringNotLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringNotLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
function Function
|
||||
expectedResult Key
|
||||
}{
|
||||
{case1Function, S3XAmzCopySource},
|
||||
{case2Function, S3XAmzServerSideEncryption},
|
||||
{case3Function, S3XAmzMetadataDirective},
|
||||
{case4Function, S3LocationConstraint},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.function.key()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringNotLikeFuncToMap(t *testing.T) {
|
||||
case1Function, err := newStringNotLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case1Result := map[Key]ValueSet{
|
||||
S3XAmzCopySource: NewValueSet(NewStringValue("mybucket/*")),
|
||||
}
|
||||
|
||||
case2Function, err := newStringNotLikeFunc(S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/*"),
|
||||
NewStringValue("yourbucket/myobject*"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Result := map[Key]ValueSet{
|
||||
S3XAmzCopySource: NewValueSet(
|
||||
NewStringValue("mybucket/*"),
|
||||
NewStringValue("yourbucket/myobject*"),
|
||||
),
|
||||
}
|
||||
|
||||
case3Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Result := map[Key]ValueSet{
|
||||
S3XAmzServerSideEncryption: NewValueSet(NewStringValue("AES*")),
|
||||
}
|
||||
|
||||
case4Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES*"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Result := map[Key]ValueSet{
|
||||
S3XAmzServerSideEncryption: NewValueSet(
|
||||
NewStringValue("AES*"),
|
||||
),
|
||||
}
|
||||
|
||||
case5Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case5Result := map[Key]ValueSet{
|
||||
S3XAmzMetadataDirective: NewValueSet(NewStringValue("REPL*")),
|
||||
}
|
||||
|
||||
case6Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPL*"),
|
||||
NewStringValue("COPY*"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case6Result := map[Key]ValueSet{
|
||||
S3XAmzMetadataDirective: NewValueSet(
|
||||
NewStringValue("REPL*"),
|
||||
NewStringValue("COPY*"),
|
||||
),
|
||||
}
|
||||
|
||||
case7Function, err := newStringNotLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case7Result := map[Key]ValueSet{
|
||||
S3LocationConstraint: NewValueSet(NewStringValue("eu-west-*")),
|
||||
}
|
||||
|
||||
case8Function, err := newStringNotLikeFunc(S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-*"),
|
||||
NewStringValue("us-west-*"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case8Result := map[Key]ValueSet{
|
||||
S3LocationConstraint: NewValueSet(
|
||||
NewStringValue("eu-west-*"),
|
||||
NewStringValue("us-west-*"),
|
||||
),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
f Function
|
||||
expectedResult map[Key]ValueSet
|
||||
}{
|
||||
{case1Function, case1Result},
|
||||
{case2Function, case2Result},
|
||||
{case3Function, case3Result},
|
||||
{case4Function, case4Result},
|
||||
{case5Function, case5Result},
|
||||
{case6Function, case6Result},
|
||||
{case7Function, case7Result},
|
||||
{case8Function, case8Result},
|
||||
{&stringNotLikeFunc{}, nil},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.f.toMap()
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewStringLikeFunc(t *testing.T) {
|
||||
case1Function, err := newStringLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringLikeFunc(S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/*"),
|
||||
NewStringValue("yourbucket/myobject*"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringLikeFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES*"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case5Function, err := newStringLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case6Function, err := newStringLikeFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPL*"),
|
||||
NewStringValue("COPY*"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case7Function, err := newStringLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case8Function, err := newStringLikeFunc(S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-*"),
|
||||
NewStringValue("us-west-*"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
key Key
|
||||
values ValueSet
|
||||
expectedResult Function
|
||||
expectErr bool
|
||||
}{
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")), case1Function, false},
|
||||
{S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/*"),
|
||||
NewStringValue("yourbucket/myobject*"),
|
||||
), case2Function, false},
|
||||
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")), case3Function, false},
|
||||
{S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES*"),
|
||||
), case4Function, false},
|
||||
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")), case5Function, false},
|
||||
{S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPL*"),
|
||||
NewStringValue("COPY*"),
|
||||
), case6Function, false},
|
||||
|
||||
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")), case7Function, false},
|
||||
{S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-*"),
|
||||
NewStringValue("us-west-*"),
|
||||
), case8Function, false},
|
||||
|
||||
// Unsupported value error.
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"), NewIntValue(7)), nil, true},
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"), NewIntValue(7)), nil, true},
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"), NewIntValue(7)), nil, true},
|
||||
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"), NewIntValue(7)), nil, true},
|
||||
|
||||
// Invalid value error.
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket")), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := newStringLikeFunc(testCase.key, testCase.values)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewStringNotLikeFunc(t *testing.T) {
|
||||
case1Function, err := newStringNotLikeFunc(S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case2Function, err := newStringNotLikeFunc(S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/*"),
|
||||
NewStringValue("yourbucket/myobject*"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case4Function, err := newStringNotLikeFunc(S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES*"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case5Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case6Function, err := newStringNotLikeFunc(S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPL*"),
|
||||
NewStringValue("COPY*"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case7Function, err := newStringNotLikeFunc(S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case8Function, err := newStringNotLikeFunc(S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-*"),
|
||||
NewStringValue("us-west-*"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
key Key
|
||||
values ValueSet
|
||||
expectedResult Function
|
||||
expectErr bool
|
||||
}{
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/*")), case1Function, false},
|
||||
{S3XAmzCopySource,
|
||||
NewValueSet(
|
||||
NewStringValue("mybucket/*"),
|
||||
NewStringValue("yourbucket/myobject*"),
|
||||
), case2Function, false},
|
||||
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES*")), case3Function, false},
|
||||
{S3XAmzServerSideEncryption,
|
||||
NewValueSet(
|
||||
NewStringValue("AES*"),
|
||||
), case4Function, false},
|
||||
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPL*")), case5Function, false},
|
||||
{S3XAmzMetadataDirective,
|
||||
NewValueSet(
|
||||
NewStringValue("REPL*"),
|
||||
NewStringValue("COPY*"),
|
||||
), case6Function, false},
|
||||
|
||||
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-*")), case7Function, false},
|
||||
{S3LocationConstraint,
|
||||
NewValueSet(
|
||||
NewStringValue("eu-west-*"),
|
||||
NewStringValue("us-west-*"),
|
||||
), case8Function, false},
|
||||
|
||||
// Unsupported value error.
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket/myobject"), NewIntValue(7)), nil, true},
|
||||
{S3XAmzServerSideEncryption, NewValueSet(NewStringValue("AES256"), NewIntValue(7)), nil, true},
|
||||
{S3XAmzMetadataDirective, NewValueSet(NewStringValue("REPLACE"), NewIntValue(7)), nil, true},
|
||||
{S3LocationConstraint, NewValueSet(NewStringValue("eu-west-1"), NewIntValue(7)), nil, true},
|
||||
|
||||
// Invalid value error.
|
||||
{S3XAmzCopySource, NewValueSet(NewStringValue("mybucket")), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := newStringNotLikeFunc(testCase.key, testCase.values)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Splits an incoming path into bucket and object components.
|
||||
func path2BucketAndObject(path string) (bucket, object string) {
|
||||
// Skip the first element if it is '/', split the rest.
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
pathComponents := strings.SplitN(path, "/", 2)
|
||||
|
||||
// Save the bucket and object extracted from path.
|
||||
switch len(pathComponents) {
|
||||
case 1:
|
||||
bucket = pathComponents[0]
|
||||
case 2:
|
||||
bucket = pathComponents[0]
|
||||
object = pathComponents[1]
|
||||
}
|
||||
return bucket, object
|
||||
}
|
||||
|
||||
// Value - is enum type of string, int or bool.
|
||||
type Value struct {
|
||||
t reflect.Kind
|
||||
s string
|
||||
i int
|
||||
b bool
|
||||
}
|
||||
|
||||
// GetBool - gets stored bool value.
|
||||
func (v Value) GetBool() (bool, error) {
|
||||
var err error
|
||||
|
||||
if v.t != reflect.Bool {
|
||||
err = fmt.Errorf("not a bool Value")
|
||||
}
|
||||
|
||||
return v.b, err
|
||||
}
|
||||
|
||||
// GetInt - gets stored int value.
|
||||
func (v Value) GetInt() (int, error) {
|
||||
var err error
|
||||
|
||||
if v.t != reflect.Int {
|
||||
err = fmt.Errorf("not a int Value")
|
||||
}
|
||||
|
||||
return v.i, err
|
||||
}
|
||||
|
||||
// GetString - gets stored string value.
|
||||
func (v Value) GetString() (string, error) {
|
||||
var err error
|
||||
|
||||
if v.t != reflect.String {
|
||||
err = fmt.Errorf("not a string Value")
|
||||
}
|
||||
|
||||
return v.s, err
|
||||
}
|
||||
|
||||
// GetType - gets enum type.
|
||||
func (v Value) GetType() reflect.Kind {
|
||||
return v.t
|
||||
}
|
||||
|
||||
// MarshalJSON - encodes Value to JSON data.
|
||||
func (v Value) MarshalJSON() ([]byte, error) {
|
||||
switch v.t {
|
||||
case reflect.String:
|
||||
return json.Marshal(v.s)
|
||||
case reflect.Int:
|
||||
return json.Marshal(v.i)
|
||||
case reflect.Bool:
|
||||
return json.Marshal(v.b)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown value kind %v", v.t)
|
||||
}
|
||||
|
||||
// StoreBool - stores bool value.
|
||||
func (v *Value) StoreBool(b bool) {
|
||||
*v = Value{t: reflect.Bool, b: b}
|
||||
}
|
||||
|
||||
// StoreInt - stores int value.
|
||||
func (v *Value) StoreInt(i int) {
|
||||
*v = Value{t: reflect.Int, i: i}
|
||||
}
|
||||
|
||||
// StoreString - stores string value.
|
||||
func (v *Value) StoreString(s string) {
|
||||
*v = Value{t: reflect.String, s: s}
|
||||
}
|
||||
|
||||
// String - returns string representation of value.
|
||||
func (v Value) String() string {
|
||||
switch v.t {
|
||||
case reflect.String:
|
||||
return v.s
|
||||
case reflect.Int:
|
||||
return strconv.Itoa(v.i)
|
||||
case reflect.Bool:
|
||||
return strconv.FormatBool(v.b)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// UnmarshalJSON - decodes JSON data.
|
||||
func (v *Value) UnmarshalJSON(data []byte) error {
|
||||
var b bool
|
||||
if err := json.Unmarshal(data, &b); err == nil {
|
||||
v.StoreBool(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
var i int
|
||||
if err := json.Unmarshal(data, &i); err == nil {
|
||||
v.StoreInt(i)
|
||||
return nil
|
||||
}
|
||||
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err == nil {
|
||||
v.StoreString(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unknown json data '%v'", data)
|
||||
}
|
||||
|
||||
// NewBoolValue - returns new bool value.
|
||||
func NewBoolValue(b bool) Value {
|
||||
value := &Value{}
|
||||
value.StoreBool(b)
|
||||
return *value
|
||||
}
|
||||
|
||||
// NewIntValue - returns new int value.
|
||||
func NewIntValue(i int) Value {
|
||||
value := &Value{}
|
||||
value.StoreInt(i)
|
||||
return *value
|
||||
}
|
||||
|
||||
// NewStringValue - returns new string value.
|
||||
func NewStringValue(s string) Value {
|
||||
value := &Value{}
|
||||
value.StoreString(s)
|
||||
return *value
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValueGetBool(t *testing.T) {
|
||||
testCases := []struct {
|
||||
value Value
|
||||
expectedResult bool
|
||||
expectErr bool
|
||||
}{
|
||||
{NewBoolValue(true), true, false},
|
||||
{NewIntValue(7), false, true},
|
||||
{Value{}, false, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := testCase.value.GetBool()
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueGetInt(t *testing.T) {
|
||||
testCases := []struct {
|
||||
value Value
|
||||
expectedResult int
|
||||
expectErr bool
|
||||
}{
|
||||
{NewIntValue(7), 7, false},
|
||||
{NewBoolValue(true), 0, true},
|
||||
{Value{}, 0, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := testCase.value.GetInt()
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueGetString(t *testing.T) {
|
||||
testCases := []struct {
|
||||
value Value
|
||||
expectedResult string
|
||||
expectErr bool
|
||||
}{
|
||||
{NewStringValue("foo"), "foo", false},
|
||||
{NewBoolValue(true), "", true},
|
||||
{Value{}, "", true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := testCase.value.GetString()
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueGetType(t *testing.T) {
|
||||
testCases := []struct {
|
||||
value Value
|
||||
expectedResult reflect.Kind
|
||||
}{
|
||||
{NewBoolValue(true), reflect.Bool},
|
||||
{NewIntValue(7), reflect.Int},
|
||||
{NewStringValue("foo"), reflect.String},
|
||||
{Value{}, reflect.Invalid},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.value.GetType()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueMarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
value Value
|
||||
expectedResult []byte
|
||||
expectErr bool
|
||||
}{
|
||||
{NewBoolValue(true), []byte("true"), false},
|
||||
{NewIntValue(7), []byte("7"), false},
|
||||
{NewStringValue("foo"), []byte(`"foo"`), false},
|
||||
{Value{}, nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := json.Marshal(testCase.value)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueStoreBool(t *testing.T) {
|
||||
testCases := []struct {
|
||||
value bool
|
||||
expectedResult Value
|
||||
}{
|
||||
{false, NewBoolValue(false)},
|
||||
{true, NewBoolValue(true)},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
var result Value
|
||||
result.StoreBool(testCase.value)
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueStoreInt(t *testing.T) {
|
||||
testCases := []struct {
|
||||
value int
|
||||
expectedResult Value
|
||||
}{
|
||||
{0, NewIntValue(0)},
|
||||
{7, NewIntValue(7)},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
var result Value
|
||||
result.StoreInt(testCase.value)
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueStoreString(t *testing.T) {
|
||||
testCases := []struct {
|
||||
value string
|
||||
expectedResult Value
|
||||
}{
|
||||
{"", NewStringValue("")},
|
||||
{"foo", NewStringValue("foo")},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
var result Value
|
||||
result.StoreString(testCase.value)
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueString(t *testing.T) {
|
||||
testCases := []struct {
|
||||
value Value
|
||||
expectedResult string
|
||||
}{
|
||||
{NewBoolValue(true), "true"},
|
||||
{NewIntValue(7), "7"},
|
||||
{NewStringValue("foo"), "foo"},
|
||||
{Value{}, ""},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.value.String()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueUnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
data []byte
|
||||
expectedResult Value
|
||||
expectErr bool
|
||||
}{
|
||||
{[]byte("true"), NewBoolValue(true), false},
|
||||
{[]byte("7"), NewIntValue(7), false},
|
||||
{[]byte(`"foo"`), NewStringValue("foo"), false},
|
||||
{[]byte("True"), Value{}, true},
|
||||
{[]byte("7.1"), Value{}, true},
|
||||
{[]byte(`["foo"]`), Value{}, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
var result Value
|
||||
err := json.Unmarshal(testCase.data, &result)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ValueSet - unique list of values.
|
||||
type ValueSet map[Value]struct{}
|
||||
|
||||
// Add - adds given value to value set.
|
||||
func (set ValueSet) Add(value Value) {
|
||||
set[value] = struct{}{}
|
||||
}
|
||||
|
||||
// MarshalJSON - encodes ValueSet to JSON data.
|
||||
func (set ValueSet) MarshalJSON() ([]byte, error) {
|
||||
var values []Value
|
||||
for k := range set {
|
||||
values = append(values, k)
|
||||
}
|
||||
|
||||
if len(values) == 0 {
|
||||
return nil, fmt.Errorf("invalid value set %v", set)
|
||||
}
|
||||
|
||||
return json.Marshal(values)
|
||||
}
|
||||
|
||||
// UnmarshalJSON - decodes JSON data.
|
||||
func (set *ValueSet) UnmarshalJSON(data []byte) error {
|
||||
var v Value
|
||||
if err := json.Unmarshal(data, &v); err == nil {
|
||||
*set = make(ValueSet)
|
||||
set.Add(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
var values []Value
|
||||
if err := json.Unmarshal(data, &values); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(values) < 1 {
|
||||
return fmt.Errorf("invalid value")
|
||||
}
|
||||
|
||||
*set = make(ValueSet)
|
||||
for _, v = range values {
|
||||
if _, found := (*set)[v]; found {
|
||||
return fmt.Errorf("duplicate value found '%v'", v)
|
||||
}
|
||||
|
||||
set.Add(v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewValueSet - returns new value set containing given values.
|
||||
func NewValueSet(values ...Value) ValueSet {
|
||||
set := make(ValueSet)
|
||||
|
||||
for _, value := range values {
|
||||
set.Add(value)
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 condition
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValueSetAdd(t *testing.T) {
|
||||
testCases := []struct {
|
||||
value Value
|
||||
expectedResult ValueSet
|
||||
}{
|
||||
{NewBoolValue(true), NewValueSet(NewBoolValue(true))},
|
||||
{NewIntValue(7), NewValueSet(NewIntValue(7))},
|
||||
{NewStringValue("foo"), NewValueSet(NewStringValue("foo"))},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := NewValueSet()
|
||||
result.Add(testCase.value)
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueSetMarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
set ValueSet
|
||||
expectedResult string
|
||||
expectErr bool
|
||||
}{
|
||||
{NewValueSet(NewBoolValue(true)), `[true]`, false},
|
||||
{NewValueSet(NewIntValue(7)), `[7]`, false},
|
||||
{NewValueSet(NewStringValue("foo")), `["foo"]`, false},
|
||||
{NewValueSet(NewBoolValue(true)), `[true]`, false},
|
||||
{NewValueSet(NewStringValue("7")), `["7"]`, false},
|
||||
{NewValueSet(NewStringValue("foo")), `["foo"]`, false},
|
||||
{make(ValueSet), "", true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := json.Marshal(testCase.set)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if string(result) != testCase.expectedResult {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, string(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueSetUnmarshalJSON(t *testing.T) {
|
||||
set1 := NewValueSet(
|
||||
NewBoolValue(true),
|
||||
NewStringValue("false"),
|
||||
NewIntValue(7),
|
||||
NewStringValue("7"),
|
||||
NewStringValue("foo"),
|
||||
NewStringValue("192.168.1.100/24"),
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
data []byte
|
||||
expectedResult ValueSet
|
||||
expectErr bool
|
||||
}{
|
||||
{[]byte(`true`), NewValueSet(NewBoolValue(true)), false},
|
||||
{[]byte(`7`), NewValueSet(NewIntValue(7)), false},
|
||||
{[]byte(`"foo"`), NewValueSet(NewStringValue("foo")), false},
|
||||
{[]byte(`[true]`), NewValueSet(NewBoolValue(true)), false},
|
||||
{[]byte(`[7]`), NewValueSet(NewIntValue(7)), false},
|
||||
{[]byte(`["foo"]`), NewValueSet(NewStringValue("foo")), false},
|
||||
{[]byte(`[true, "false", 7, "7", "foo", "192.168.1.100/24"]`), set1, false},
|
||||
{[]byte(`{}`), nil, true}, // Unsupported data.
|
||||
{[]byte(`[]`), nil, true}, // Empty array.
|
||||
{[]byte(`[7, 7, true]`), nil, true}, // Duplicate value.
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := make(ValueSet)
|
||||
err := json.Unmarshal(testCase.data, &result)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Effect - policy statement effect Allow or Deny.
|
||||
type Effect string
|
||||
|
||||
const (
|
||||
// Allow - allow effect.
|
||||
Allow Effect = "Allow"
|
||||
|
||||
// Deny - deny effect.
|
||||
Deny = "Deny"
|
||||
)
|
||||
|
||||
// IsAllowed - returns if given check is allowed or not.
|
||||
func (effect Effect) IsAllowed(b bool) bool {
|
||||
if effect == Allow {
|
||||
return b
|
||||
}
|
||||
|
||||
return !b
|
||||
}
|
||||
|
||||
// IsValid - checks if Effect is valid or not
|
||||
func (effect Effect) IsValid() bool {
|
||||
switch effect {
|
||||
case Allow, Deny:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON - encodes Effect to JSON data.
|
||||
func (effect Effect) MarshalJSON() ([]byte, error) {
|
||||
if !effect.IsValid() {
|
||||
return nil, Errorf("invalid effect '%v'", effect)
|
||||
}
|
||||
|
||||
return json.Marshal(string(effect))
|
||||
}
|
||||
|
||||
// UnmarshalJSON - decodes JSON data to Effect.
|
||||
func (effect *Effect) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e := Effect(s)
|
||||
if !e.IsValid() {
|
||||
return Errorf("invalid effect '%v'", s)
|
||||
}
|
||||
|
||||
*effect = e
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEffectIsAllowed(t *testing.T) {
|
||||
testCases := []struct {
|
||||
effect Effect
|
||||
check bool
|
||||
expectedResult bool
|
||||
}{
|
||||
{Allow, false, false},
|
||||
{Allow, true, true},
|
||||
{Deny, false, true},
|
||||
{Deny, true, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.effect.IsAllowed(testCase.check)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestEffectIsValid(t *testing.T) {
|
||||
testCases := []struct {
|
||||
effect Effect
|
||||
expectedResult bool
|
||||
}{
|
||||
{Allow, true},
|
||||
{Deny, true},
|
||||
{Effect(""), false},
|
||||
{Effect("foo"), false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.effect.IsValid()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectMarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
effect Effect
|
||||
expectedResult []byte
|
||||
expectErr bool
|
||||
}{
|
||||
{Allow, []byte(`"Allow"`), false},
|
||||
{Deny, []byte(`"Deny"`), false},
|
||||
{Effect(""), nil, true},
|
||||
{Effect("foo"), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := json.Marshal(testCase.effect)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, string(testCase.expectedResult), string(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectUnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
data []byte
|
||||
expectedResult Effect
|
||||
expectErr bool
|
||||
}{
|
||||
{[]byte(`"Allow"`), Allow, false},
|
||||
{[]byte(`"Deny"`), Deny, false},
|
||||
{[]byte(`""`), Effect(""), true},
|
||||
{[]byte(`"foo"`), Effect(""), true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
var result Effect
|
||||
err := json.Unmarshal(testCase.data, &result)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 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 policy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Error is the generic type for any error happening during policy
|
||||
// parsing.
|
||||
type Error struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// Errorf - formats according to a format specifier and returns
|
||||
// the string as a value that satisfies error of type policy.Error
|
||||
func Errorf(format string, a ...interface{}) error {
|
||||
return Error{err: fmt.Errorf(format, a...)}
|
||||
}
|
||||
|
||||
// Unwrap the internal error.
|
||||
func (e Error) Unwrap() error { return e.err }
|
||||
|
||||
// Error 'error' compatible method.
|
||||
func (e Error) Error() string {
|
||||
if e.err == nil {
|
||||
return "policy: cause <nil>"
|
||||
}
|
||||
return e.err.Error()
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// ID - policy ID.
|
||||
type ID string
|
||||
|
||||
// IsValid - checks if ID is valid or not.
|
||||
func (id ID) IsValid() bool {
|
||||
return utf8.ValidString(string(id))
|
||||
}
|
||||
|
||||
// MarshalJSON - encodes ID to JSON data.
|
||||
func (id ID) MarshalJSON() ([]byte, error) {
|
||||
if !id.IsValid() {
|
||||
return nil, Errorf("invalid ID %v", id)
|
||||
}
|
||||
|
||||
return json.Marshal(string(id))
|
||||
}
|
||||
|
||||
// UnmarshalJSON - decodes JSON data to ID.
|
||||
func (id *ID) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i := ID(s)
|
||||
if !i.IsValid() {
|
||||
return Errorf("invalid ID %v", s)
|
||||
}
|
||||
|
||||
*id = i
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIDIsValid(t *testing.T) {
|
||||
testCases := []struct {
|
||||
id ID
|
||||
expectedResult bool
|
||||
}{
|
||||
{ID("DenyEncryptionSt1"), true},
|
||||
{ID(""), true},
|
||||
{ID("aa\xe2"), false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.id.IsValid()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Errorf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIDMarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
id ID
|
||||
expectedResult []byte
|
||||
expectErr bool
|
||||
}{
|
||||
{ID("DenyEncryptionSt1"), []byte(`"DenyEncryptionSt1"`), false},
|
||||
{ID(""), []byte(`""`), false},
|
||||
{ID("aa\xe2"), nil, true}, // invalid utf-8
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := json.Marshal(testCase.id)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, string(testCase.expectedResult), string(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIDUnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
data []byte
|
||||
expectedResult ID
|
||||
expectErr bool
|
||||
}{
|
||||
{[]byte(`"DenyEncryptionSt1"`), ID("DenyEncryptionSt1"), false},
|
||||
{[]byte(`""`), ID(""), false},
|
||||
{[]byte(`"aa\xe2"`), ID(""), true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
var result ID
|
||||
err := json.Unmarshal(testCase.data, &result)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
// DefaultVersion - default policy version as per AWS S3 specification.
|
||||
const DefaultVersion = "2012-10-17"
|
||||
|
||||
// Args - arguments to policy to check whether it is allowed
|
||||
type Args struct {
|
||||
AccountName string `json:"account"`
|
||||
Action Action `json:"action"`
|
||||
BucketName string `json:"bucket"`
|
||||
ConditionValues map[string][]string `json:"conditions"`
|
||||
IsOwner bool `json:"owner"`
|
||||
ObjectName string `json:"object"`
|
||||
}
|
||||
|
||||
// Policy - bucket policy.
|
||||
type Policy struct {
|
||||
ID ID `json:"ID,omitempty"`
|
||||
Version string
|
||||
Statements []Statement `json:"Statement"`
|
||||
}
|
||||
|
||||
// IsAllowed - checks given policy args is allowed to continue the Rest API.
|
||||
func (policy Policy) IsAllowed(args Args) bool {
|
||||
// Check all deny statements. If any one statement denies, return false.
|
||||
for _, statement := range policy.Statements {
|
||||
if statement.Effect == Deny {
|
||||
if !statement.IsAllowed(args) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For owner, its allowed by default.
|
||||
if args.IsOwner {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check all allow statements. If any one statement allows, return true.
|
||||
for _, statement := range policy.Statements {
|
||||
if statement.Effect == Allow {
|
||||
if statement.IsAllowed(args) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsEmpty - returns whether policy is empty or not.
|
||||
func (policy Policy) IsEmpty() bool {
|
||||
return len(policy.Statements) == 0
|
||||
}
|
||||
|
||||
// isValid - checks if Policy is valid or not.
|
||||
func (policy Policy) isValid() error {
|
||||
if policy.Version != DefaultVersion && policy.Version != "" {
|
||||
return Errorf("invalid version '%v'", policy.Version)
|
||||
}
|
||||
|
||||
for _, statement := range policy.Statements {
|
||||
if err := statement.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for i := range policy.Statements {
|
||||
for _, statement := range policy.Statements[i+1:] {
|
||||
if policy.Statements[i].Effect != statement.Effect {
|
||||
continue
|
||||
}
|
||||
|
||||
if !policy.Statements[i].Principal.Equals(statement.Principal) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !policy.Statements[i].Actions.Equals(statement.Actions) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !policy.Statements[i].Resources.Equals(statement.Resources) {
|
||||
continue
|
||||
}
|
||||
|
||||
if policy.Statements[i].Conditions.String() != statement.Conditions.String() {
|
||||
continue
|
||||
}
|
||||
|
||||
return Errorf("duplicate principal %v, actions %v, resouces %v found in statements %v, %v",
|
||||
statement.Principal, statement.Actions,
|
||||
statement.Resources, policy.Statements[i],
|
||||
statement)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON - encodes Policy to JSON data.
|
||||
func (policy Policy) MarshalJSON() ([]byte, error) {
|
||||
if err := policy.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// subtype to avoid recursive call to MarshalJSON()
|
||||
type subPolicy Policy
|
||||
return json.Marshal(subPolicy(policy))
|
||||
}
|
||||
|
||||
// UnmarshalJSON - decodes JSON data to Policy.
|
||||
func (policy *Policy) UnmarshalJSON(data []byte) error {
|
||||
// subtype to avoid recursive call to UnmarshalJSON()
|
||||
type subPolicy Policy
|
||||
var sp subPolicy
|
||||
if err := json.Unmarshal(data, &sp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p := Policy(sp)
|
||||
if err := p.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*policy = p
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate - validates all statements are for given bucket or not.
|
||||
func (policy Policy) Validate(bucketName string) error {
|
||||
if err := policy.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, statement := range policy.Statements {
|
||||
if err := statement.Validate(bucketName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseConfig - parses data in given reader to Policy.
|
||||
func ParseConfig(reader io.Reader, bucketName string) (*Policy, error) {
|
||||
var policy Policy
|
||||
|
||||
decoder := json.NewDecoder(reader)
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&policy); err != nil {
|
||||
return nil, Errorf("%w", err)
|
||||
}
|
||||
|
||||
err := policy.Validate(bucketName)
|
||||
return &policy, err
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/pkg/wildcard"
|
||||
)
|
||||
|
||||
// Principal - policy principal.
|
||||
type Principal struct {
|
||||
AWS set.StringSet
|
||||
}
|
||||
|
||||
// IsValid - checks whether Principal is valid or not.
|
||||
func (p Principal) IsValid() bool {
|
||||
return len(p.AWS) != 0
|
||||
}
|
||||
|
||||
// Equals - returns true if principals are equal.
|
||||
func (p Principal) Equals(pp Principal) bool {
|
||||
return p.AWS.Equals(pp.AWS)
|
||||
}
|
||||
|
||||
// Intersection - returns principals available in both Principal.
|
||||
func (p Principal) Intersection(principal Principal) set.StringSet {
|
||||
return p.AWS.Intersection(principal.AWS)
|
||||
}
|
||||
|
||||
// MarshalJSON - encodes Principal to JSON data.
|
||||
func (p Principal) MarshalJSON() ([]byte, error) {
|
||||
if !p.IsValid() {
|
||||
return nil, Errorf("invalid principal %v", p)
|
||||
}
|
||||
|
||||
// subtype to avoid recursive call to MarshalJSON()
|
||||
type subPrincipal Principal
|
||||
sp := subPrincipal(p)
|
||||
return json.Marshal(sp)
|
||||
}
|
||||
|
||||
// Match - matches given principal is wildcard matching with Principal.
|
||||
func (p Principal) Match(principal string) bool {
|
||||
for _, pattern := range p.AWS.ToSlice() {
|
||||
if wildcard.MatchSimple(pattern, principal) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// UnmarshalJSON - decodes JSON data to Principal.
|
||||
func (p *Principal) UnmarshalJSON(data []byte) error {
|
||||
// subtype to avoid recursive call to UnmarshalJSON()
|
||||
type subPrincipal Principal
|
||||
var sp subPrincipal
|
||||
|
||||
if err := json.Unmarshal(data, &sp); err != nil {
|
||||
var s string
|
||||
if err = json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s != "*" {
|
||||
return Errorf("invalid principal '%v'", s)
|
||||
}
|
||||
|
||||
sp.AWS = set.CreateStringSet("*")
|
||||
}
|
||||
|
||||
*p = Principal(sp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPrincipal - creates new Principal.
|
||||
func NewPrincipal(principals ...string) Principal {
|
||||
return Principal{AWS: set.CreateStringSet(principals...)}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
)
|
||||
|
||||
func TestPrincipalIsValid(t *testing.T) {
|
||||
testCases := []struct {
|
||||
principal Principal
|
||||
expectedResult bool
|
||||
}{
|
||||
{NewPrincipal("*"), true},
|
||||
{NewPrincipal("arn:aws:iam::AccountNumber:root"), true},
|
||||
{NewPrincipal(), false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.principal.IsValid()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrincipalIntersection(t *testing.T) {
|
||||
testCases := []struct {
|
||||
principal Principal
|
||||
principalToIntersect Principal
|
||||
expectedResult set.StringSet
|
||||
}{
|
||||
{NewPrincipal("*"), NewPrincipal("*"), set.CreateStringSet("*")},
|
||||
{NewPrincipal("arn:aws:iam::AccountNumber:root"), NewPrincipal("arn:aws:iam::AccountNumber:myuser"), set.CreateStringSet()},
|
||||
{NewPrincipal(), NewPrincipal("*"), set.CreateStringSet()},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.principal.Intersection(testCase.principalToIntersect)
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrincipalMarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
principal Principal
|
||||
expectedResult []byte
|
||||
expectErr bool
|
||||
}{
|
||||
{NewPrincipal("*"), []byte(`{"AWS":["*"]}`), false},
|
||||
{NewPrincipal("arn:aws:iam::AccountNumber:*"), []byte(`{"AWS":["arn:aws:iam::AccountNumber:*"]}`), false},
|
||||
{NewPrincipal(), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := json.Marshal(testCase.principal)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, string(testCase.expectedResult), string(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrincipalMatch(t *testing.T) {
|
||||
testCases := []struct {
|
||||
principals Principal
|
||||
principal string
|
||||
expectedResult bool
|
||||
}{
|
||||
{NewPrincipal("*"), "AccountNumber", true},
|
||||
{NewPrincipal("arn:aws:iam::*"), "arn:aws:iam::AccountNumber:root", true},
|
||||
{NewPrincipal("arn:aws:iam::AccountNumber:*"), "arn:aws:iam::TestAccountNumber:root", false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.principals.Match(testCase.principal)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrincipalUnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
data []byte
|
||||
expectedResult Principal
|
||||
expectErr bool
|
||||
}{
|
||||
{[]byte(`"*"`), NewPrincipal("*"), false},
|
||||
{[]byte(`{"AWS": "*"}`), NewPrincipal("*"), false},
|
||||
{[]byte(`{"AWS": "arn:aws:iam::AccountNumber:*"}`), NewPrincipal("arn:aws:iam::AccountNumber:*"), false},
|
||||
{[]byte(`"arn:aws:iam::AccountNumber:*"`), NewPrincipal(), true},
|
||||
{[]byte(`["arn:aws:iam::AccountNumber:*", "arn:aws:iam:AnotherAccount:*"]`), NewPrincipal(), true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
var result Principal
|
||||
err := json.Unmarshal(testCase.data, &result)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/pkg/bucket/policy/condition"
|
||||
"github.com/minio/minio/pkg/wildcard"
|
||||
)
|
||||
|
||||
// ResourceARNPrefix - resource ARN prefix as per AWS S3 specification.
|
||||
const ResourceARNPrefix = "arn:aws:s3:::"
|
||||
|
||||
// Resource - resource in policy statement.
|
||||
type Resource struct {
|
||||
BucketName string
|
||||
Pattern string
|
||||
}
|
||||
|
||||
func (r Resource) isBucketPattern() bool {
|
||||
return !strings.Contains(r.Pattern, "/")
|
||||
}
|
||||
|
||||
func (r Resource) isObjectPattern() bool {
|
||||
return strings.Contains(r.Pattern, "/") || strings.Contains(r.BucketName, "*")
|
||||
}
|
||||
|
||||
// IsValid - checks whether Resource is valid or not.
|
||||
func (r Resource) IsValid() bool {
|
||||
return r.BucketName != "" && r.Pattern != ""
|
||||
}
|
||||
|
||||
// Match - matches object name with resource pattern.
|
||||
func (r Resource) Match(resource string, conditionValues map[string][]string) bool {
|
||||
pattern := r.Pattern
|
||||
for _, key := range condition.CommonKeys {
|
||||
// Empty values are not supported for policy variables.
|
||||
if rvalues, ok := conditionValues[key.Name()]; ok && rvalues[0] != "" {
|
||||
pattern = strings.Replace(pattern, key.VarName(), rvalues[0], -1)
|
||||
}
|
||||
}
|
||||
|
||||
return wildcard.Match(pattern, resource)
|
||||
}
|
||||
|
||||
// MarshalJSON - encodes Resource to JSON data.
|
||||
func (r Resource) MarshalJSON() ([]byte, error) {
|
||||
if !r.IsValid() {
|
||||
return nil, Errorf("invalid resource %v", r)
|
||||
}
|
||||
|
||||
return json.Marshal(r.String())
|
||||
}
|
||||
|
||||
func (r Resource) String() string {
|
||||
return ResourceARNPrefix + r.Pattern
|
||||
}
|
||||
|
||||
// UnmarshalJSON - decodes JSON data to Resource.
|
||||
func (r *Resource) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsedResource, err := parseResource(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*r = parsedResource
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate - validates Resource is for given bucket or not.
|
||||
func (r Resource) Validate(bucketName string) error {
|
||||
if !r.IsValid() {
|
||||
return Errorf("invalid resource")
|
||||
}
|
||||
|
||||
if !wildcard.Match(r.BucketName, bucketName) {
|
||||
return Errorf("bucket name does not match")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseResource - parses string to Resource.
|
||||
func parseResource(s string) (Resource, error) {
|
||||
if !strings.HasPrefix(s, ResourceARNPrefix) {
|
||||
return Resource{}, Errorf("invalid resource '%v'", s)
|
||||
}
|
||||
|
||||
pattern := strings.TrimPrefix(s, ResourceARNPrefix)
|
||||
tokens := strings.SplitN(pattern, "/", 2)
|
||||
bucketName := tokens[0]
|
||||
if bucketName == "" {
|
||||
return Resource{}, Errorf("invalid resource format '%v'", s)
|
||||
}
|
||||
|
||||
return Resource{
|
||||
BucketName: bucketName,
|
||||
Pattern: pattern,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewResource - creates new resource.
|
||||
func NewResource(bucketName, keyName string) Resource {
|
||||
pattern := bucketName
|
||||
if keyName != "" {
|
||||
if !strings.HasPrefix(keyName, "/") {
|
||||
pattern += "/"
|
||||
}
|
||||
|
||||
pattern += keyName
|
||||
}
|
||||
|
||||
return Resource{
|
||||
BucketName: bucketName,
|
||||
Pattern: pattern,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResourceIsBucketPattern(t *testing.T) {
|
||||
testCases := []struct {
|
||||
resource Resource
|
||||
expectedResult bool
|
||||
}{
|
||||
{NewResource("*", ""), true},
|
||||
{NewResource("mybucket", ""), true},
|
||||
{NewResource("mybucket*", ""), true},
|
||||
{NewResource("mybucket?0", ""), true},
|
||||
{NewResource("", "*"), false},
|
||||
{NewResource("*", "*"), false},
|
||||
{NewResource("mybucket", "*"), false},
|
||||
{NewResource("mybucket*", "/myobject"), false},
|
||||
{NewResource("mybucket?0", "/2010/photos/*"), false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.resource.isBucketPattern()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceIsObjectPattern(t *testing.T) {
|
||||
testCases := []struct {
|
||||
resource Resource
|
||||
expectedResult bool
|
||||
}{
|
||||
{NewResource("*", ""), true},
|
||||
{NewResource("mybucket*", ""), true},
|
||||
{NewResource("", "*"), true},
|
||||
{NewResource("*", "*"), true},
|
||||
{NewResource("mybucket", "*"), true},
|
||||
{NewResource("mybucket*", "/myobject"), true},
|
||||
{NewResource("mybucket?0", "/2010/photos/*"), true},
|
||||
{NewResource("mybucket", ""), false},
|
||||
{NewResource("mybucket?0", ""), false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.resource.isObjectPattern()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceIsValid(t *testing.T) {
|
||||
testCases := []struct {
|
||||
resource Resource
|
||||
expectedResult bool
|
||||
}{
|
||||
{NewResource("*", ""), true},
|
||||
{NewResource("mybucket*", ""), true},
|
||||
{NewResource("*", "*"), true},
|
||||
{NewResource("mybucket", "*"), true},
|
||||
{NewResource("mybucket*", "/myobject"), true},
|
||||
{NewResource("mybucket?0", "/2010/photos/*"), true},
|
||||
{NewResource("mybucket", ""), true},
|
||||
{NewResource("mybucket?0", ""), true},
|
||||
{NewResource("", ""), false},
|
||||
{NewResource("", "*"), false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.resource.IsValid()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceMatch(t *testing.T) {
|
||||
testCases := []struct {
|
||||
resource Resource
|
||||
objectName string
|
||||
expectedResult bool
|
||||
}{
|
||||
{NewResource("*", ""), "mybucket", true},
|
||||
{NewResource("*", ""), "mybucket/myobject", true},
|
||||
{NewResource("mybucket*", ""), "mybucket", true},
|
||||
{NewResource("mybucket*", ""), "mybucket/myobject", true},
|
||||
{NewResource("", "*"), "/myobject", true},
|
||||
{NewResource("*", "*"), "mybucket/myobject", true},
|
||||
{NewResource("mybucket", "*"), "mybucket/myobject", true},
|
||||
{NewResource("mybucket*", "/myobject"), "mybucket/myobject", true},
|
||||
{NewResource("mybucket*", "/myobject"), "mybucket100/myobject", true},
|
||||
{NewResource("mybucket?0", "/2010/photos/*"), "mybucket20/2010/photos/1.jpg", true},
|
||||
{NewResource("mybucket", ""), "mybucket", true},
|
||||
{NewResource("mybucket?0", ""), "mybucket30", true},
|
||||
{NewResource("", "*"), "mybucket/myobject", false},
|
||||
{NewResource("*", "*"), "mybucket", false},
|
||||
{NewResource("mybucket", "*"), "mybucket10/myobject", false},
|
||||
{NewResource("mybucket?0", "/2010/photos/*"), "mybucket0/2010/photos/1.jpg", false},
|
||||
{NewResource("mybucket", ""), "mybucket/myobject", false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.resource.Match(testCase.objectName, nil)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceMarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
resource Resource
|
||||
expectedResult []byte
|
||||
expectErr bool
|
||||
}{
|
||||
{NewResource("*", ""), []byte(`"arn:aws:s3:::*"`), false},
|
||||
{NewResource("mybucket*", ""), []byte(`"arn:aws:s3:::mybucket*"`), false},
|
||||
{NewResource("mybucket", ""), []byte(`"arn:aws:s3:::mybucket"`), false},
|
||||
{NewResource("*", "*"), []byte(`"arn:aws:s3:::*/*"`), false},
|
||||
{NewResource("mybucket", "*"), []byte(`"arn:aws:s3:::mybucket/*"`), false},
|
||||
{NewResource("mybucket*", "myobject"), []byte(`"arn:aws:s3:::mybucket*/myobject"`), false},
|
||||
{NewResource("mybucket?0", "/2010/photos/*"), []byte(`"arn:aws:s3:::mybucket?0/2010/photos/*"`), false},
|
||||
{Resource{}, nil, true},
|
||||
{NewResource("", "*"), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := json.Marshal(testCase.resource)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceUnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
data []byte
|
||||
expectedResult Resource
|
||||
expectErr bool
|
||||
}{
|
||||
{[]byte(`"arn:aws:s3:::*"`), NewResource("*", ""), false},
|
||||
{[]byte(`"arn:aws:s3:::mybucket*"`), NewResource("mybucket*", ""), false},
|
||||
{[]byte(`"arn:aws:s3:::mybucket"`), NewResource("mybucket", ""), false},
|
||||
{[]byte(`"arn:aws:s3:::*/*"`), NewResource("*", "*"), false},
|
||||
{[]byte(`"arn:aws:s3:::mybucket/*"`), NewResource("mybucket", "*"), false},
|
||||
{[]byte(`"arn:aws:s3:::mybucket*/myobject"`), NewResource("mybucket*", "myobject"), false},
|
||||
{[]byte(`"arn:aws:s3:::mybucket?0/2010/photos/*"`), NewResource("mybucket?0", "/2010/photos/*"), false},
|
||||
{[]byte(`"mybucket/myobject*"`), Resource{}, true},
|
||||
{[]byte(`"arn:aws:s3:::/*"`), Resource{}, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
var result Resource
|
||||
err := json.Unmarshal(testCase.data, &result)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceValidate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
resource Resource
|
||||
bucketName string
|
||||
expectErr bool
|
||||
}{
|
||||
{NewResource("mybucket", "/myobject*"), "mybucket", false},
|
||||
{NewResource("", "/myobject*"), "yourbucket", true},
|
||||
{NewResource("mybucket", "/myobject*"), "yourbucket", true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := testCase.resource.Validate(testCase.bucketName)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
)
|
||||
|
||||
// ResourceSet - set of resources in policy statement.
|
||||
type ResourceSet map[Resource]struct{}
|
||||
|
||||
// bucketResourceExists - checks if at least one bucket resource exists in the set.
|
||||
func (resourceSet ResourceSet) bucketResourceExists() bool {
|
||||
for resource := range resourceSet {
|
||||
if resource.isBucketPattern() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// objectResourceExists - checks if at least one object resource exists in the set.
|
||||
func (resourceSet ResourceSet) objectResourceExists() bool {
|
||||
for resource := range resourceSet {
|
||||
if resource.isObjectPattern() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Add - adds resource to resource set.
|
||||
func (resourceSet ResourceSet) Add(resource Resource) {
|
||||
resourceSet[resource] = struct{}{}
|
||||
}
|
||||
|
||||
// Equals - checks whether given resource set is equal to current resource set or not.
|
||||
func (resourceSet ResourceSet) Equals(sresourceSet ResourceSet) bool {
|
||||
// If length of set is not equal to length of given set, the
|
||||
// set is not equal to given set.
|
||||
if len(resourceSet) != len(sresourceSet) {
|
||||
return false
|
||||
}
|
||||
|
||||
// As both sets are equal in length, check each elements are equal.
|
||||
for k := range resourceSet {
|
||||
if _, ok := sresourceSet[k]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Intersection - returns resouces available in both ResourcsSet.
|
||||
func (resourceSet ResourceSet) Intersection(sset ResourceSet) ResourceSet {
|
||||
nset := NewResourceSet()
|
||||
for k := range resourceSet {
|
||||
if _, ok := sset[k]; ok {
|
||||
nset.Add(k)
|
||||
}
|
||||
}
|
||||
|
||||
return nset
|
||||
}
|
||||
|
||||
// MarshalJSON - encodes ResourceSet to JSON data.
|
||||
func (resourceSet ResourceSet) MarshalJSON() ([]byte, error) {
|
||||
if len(resourceSet) == 0 {
|
||||
return nil, Errorf("empty resources not allowed")
|
||||
}
|
||||
|
||||
resources := []Resource{}
|
||||
for resource := range resourceSet {
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
|
||||
return json.Marshal(resources)
|
||||
}
|
||||
|
||||
// Match - matches object name with anyone of resource pattern in resource set.
|
||||
func (resourceSet ResourceSet) Match(resource string, conditionValues map[string][]string) bool {
|
||||
for r := range resourceSet {
|
||||
if r.Match(resource, conditionValues) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (resourceSet ResourceSet) String() string {
|
||||
resources := []string{}
|
||||
for resource := range resourceSet {
|
||||
resources = append(resources, resource.String())
|
||||
}
|
||||
sort.Strings(resources)
|
||||
|
||||
return fmt.Sprintf("%v", resources)
|
||||
}
|
||||
|
||||
// UnmarshalJSON - decodes JSON data to ResourceSet.
|
||||
func (resourceSet *ResourceSet) UnmarshalJSON(data []byte) error {
|
||||
var sset set.StringSet
|
||||
if err := json.Unmarshal(data, &sset); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*resourceSet = make(ResourceSet)
|
||||
for _, s := range sset.ToSlice() {
|
||||
resource, err := parseResource(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, found := (*resourceSet)[resource]; found {
|
||||
return Errorf("duplicate resource '%v' found", s)
|
||||
}
|
||||
|
||||
resourceSet.Add(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate - validates ResourceSet is for given bucket or not.
|
||||
func (resourceSet ResourceSet) Validate(bucketName string) error {
|
||||
for resource := range resourceSet {
|
||||
if err := resource.Validate(bucketName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewResourceSet - creates new resource set.
|
||||
func NewResourceSet(resources ...Resource) ResourceSet {
|
||||
resourceSet := make(ResourceSet)
|
||||
for _, resource := range resources {
|
||||
resourceSet.Add(resource)
|
||||
}
|
||||
|
||||
return resourceSet
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResourceSetBucketResourceExists(t *testing.T) {
|
||||
testCases := []struct {
|
||||
resourceSet ResourceSet
|
||||
expectedResult bool
|
||||
}{
|
||||
{NewResourceSet(NewResource("*", "")), true},
|
||||
{NewResourceSet(NewResource("mybucket", "")), true},
|
||||
{NewResourceSet(NewResource("mybucket*", "")), true},
|
||||
{NewResourceSet(NewResource("mybucket?0", "")), true},
|
||||
{NewResourceSet(NewResource("mybucket", "/2010/photos/*"), NewResource("mybucket", "")), true},
|
||||
{NewResourceSet(NewResource("", "*")), false},
|
||||
{NewResourceSet(NewResource("*", "*")), false},
|
||||
{NewResourceSet(NewResource("mybucket", "*")), false},
|
||||
{NewResourceSet(NewResource("mybucket*", "/myobject")), false},
|
||||
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*")), false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.resourceSet.bucketResourceExists()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceSetObjectResourceExists(t *testing.T) {
|
||||
testCases := []struct {
|
||||
resourceSet ResourceSet
|
||||
expectedResult bool
|
||||
}{
|
||||
{NewResourceSet(NewResource("*", "")), true},
|
||||
{NewResourceSet(NewResource("mybucket*", "")), true},
|
||||
{NewResourceSet(NewResource("", "*")), true},
|
||||
{NewResourceSet(NewResource("*", "*")), true},
|
||||
{NewResourceSet(NewResource("mybucket", "*")), true},
|
||||
{NewResourceSet(NewResource("mybucket*", "/myobject")), true},
|
||||
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*")), true},
|
||||
{NewResourceSet(NewResource("mybucket", ""), NewResource("mybucket", "/2910/photos/*")), true},
|
||||
{NewResourceSet(NewResource("mybucket", "")), false},
|
||||
{NewResourceSet(NewResource("mybucket?0", "")), false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.resourceSet.objectResourceExists()
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceSetAdd(t *testing.T) {
|
||||
testCases := []struct {
|
||||
resourceSet ResourceSet
|
||||
resource Resource
|
||||
expectedResult ResourceSet
|
||||
}{
|
||||
{NewResourceSet(), NewResource("mybucket", "/myobject*"),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*"))},
|
||||
{NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
NewResource("mybucket", "/yourobject*"),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*"),
|
||||
NewResource("mybucket", "/yourobject*"))},
|
||||
{NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
NewResource("mybucket", "/myobject*"),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*"))},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
testCase.resourceSet.Add(testCase.resource)
|
||||
|
||||
if !reflect.DeepEqual(testCase.resourceSet, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, testCase.resourceSet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceSetIntersection(t *testing.T) {
|
||||
testCases := []struct {
|
||||
set ResourceSet
|
||||
setToIntersect ResourceSet
|
||||
expectedResult ResourceSet
|
||||
}{
|
||||
{NewResourceSet(), NewResourceSet(NewResource("mybucket", "/myobject*")), NewResourceSet()},
|
||||
{NewResourceSet(NewResource("mybucket", "/myobject*")), NewResourceSet(), NewResourceSet()},
|
||||
{NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*"), NewResource("mybucket", "/yourobject*")),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*"))},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.set.Intersection(testCase.setToIntersect)
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceSetMarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
resoruceSet ResourceSet
|
||||
expectedResult []byte
|
||||
expectErr bool
|
||||
}{
|
||||
{NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
[]byte(`["arn:aws:s3:::mybucket/myobject*"]`), false},
|
||||
{NewResourceSet(NewResource("mybucket", "/photos/myobject*")),
|
||||
[]byte(`["arn:aws:s3:::mybucket/photos/myobject*"]`), false},
|
||||
{NewResourceSet(), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := json.Marshal(testCase.resoruceSet)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceSetMatch(t *testing.T) {
|
||||
testCases := []struct {
|
||||
resourceSet ResourceSet
|
||||
resource string
|
||||
expectedResult bool
|
||||
}{
|
||||
{NewResourceSet(NewResource("*", "")), "mybucket", true},
|
||||
{NewResourceSet(NewResource("*", "")), "mybucket/myobject", true},
|
||||
{NewResourceSet(NewResource("mybucket*", "")), "mybucket", true},
|
||||
{NewResourceSet(NewResource("mybucket*", "")), "mybucket/myobject", true},
|
||||
{NewResourceSet(NewResource("", "*")), "/myobject", true},
|
||||
{NewResourceSet(NewResource("*", "*")), "mybucket/myobject", true},
|
||||
{NewResourceSet(NewResource("mybucket", "*")), "mybucket/myobject", true},
|
||||
{NewResourceSet(NewResource("mybucket*", "/myobject")), "mybucket/myobject", true},
|
||||
{NewResourceSet(NewResource("mybucket*", "/myobject")), "mybucket100/myobject", true},
|
||||
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*")), "mybucket20/2010/photos/1.jpg", true},
|
||||
{NewResourceSet(NewResource("mybucket", "")), "mybucket", true},
|
||||
{NewResourceSet(NewResource("mybucket?0", "")), "mybucket30", true},
|
||||
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*"),
|
||||
NewResource("mybucket", "/2010/photos/*")), "mybucket/2010/photos/1.jpg", true},
|
||||
{NewResourceSet(NewResource("", "*")), "mybucket/myobject", false},
|
||||
{NewResourceSet(NewResource("*", "*")), "mybucket", false},
|
||||
{NewResourceSet(NewResource("mybucket", "*")), "mybucket10/myobject", false},
|
||||
{NewResourceSet(NewResource("mybucket", "")), "mybucket/myobject", false},
|
||||
{NewResourceSet(), "mybucket/myobject", false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.resourceSet.Match(testCase.resource, nil)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceSetUnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
data []byte
|
||||
expectedResult ResourceSet
|
||||
expectErr bool
|
||||
}{
|
||||
{[]byte(`"arn:aws:s3:::mybucket/myobject*"`),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")), false},
|
||||
{[]byte(`"arn:aws:s3:::mybucket/photos/myobject*"`),
|
||||
NewResourceSet(NewResource("mybucket", "/photos/myobject*")), false},
|
||||
{[]byte(`"arn:aws:s3:::mybucket"`), NewResourceSet(NewResource("mybucket", "")), false},
|
||||
{[]byte(`"mybucket/myobject*"`), nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
var result ResourceSet
|
||||
err := json.Unmarshal(testCase.data, &result)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceSetValidate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
resourceSet ResourceSet
|
||||
bucketName string
|
||||
expectErr bool
|
||||
}{
|
||||
{NewResourceSet(NewResource("mybucket", "/myobject*")), "mybucket", false},
|
||||
{NewResourceSet(NewResource("", "/myobject*")), "yourbucket", true},
|
||||
{NewResourceSet(NewResource("mybucket", "/myobject*")), "yourbucket", true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := testCase.resourceSet.Validate(testCase.bucketName)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/pkg/bucket/policy/condition"
|
||||
)
|
||||
|
||||
// Statement - policy statement.
|
||||
type Statement struct {
|
||||
SID ID `json:"Sid,omitempty"`
|
||||
Effect Effect `json:"Effect"`
|
||||
Principal Principal `json:"Principal"`
|
||||
Actions ActionSet `json:"Action"`
|
||||
Resources ResourceSet `json:"Resource"`
|
||||
Conditions condition.Functions `json:"Condition,omitempty"`
|
||||
}
|
||||
|
||||
// IsAllowed - checks given policy args is allowed to continue the Rest API.
|
||||
func (statement Statement) IsAllowed(args Args) bool {
|
||||
check := func() bool {
|
||||
if !statement.Principal.Match(args.AccountName) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !statement.Actions.Contains(args.Action) {
|
||||
return false
|
||||
}
|
||||
|
||||
resource := args.BucketName
|
||||
if args.ObjectName != "" {
|
||||
if !strings.HasPrefix(args.ObjectName, "/") {
|
||||
resource += "/"
|
||||
}
|
||||
|
||||
resource += args.ObjectName
|
||||
}
|
||||
|
||||
if !statement.Resources.Match(resource, args.ConditionValues) {
|
||||
return false
|
||||
}
|
||||
|
||||
return statement.Conditions.Evaluate(args.ConditionValues)
|
||||
}
|
||||
|
||||
return statement.Effect.IsAllowed(check())
|
||||
}
|
||||
|
||||
// isValid - checks whether statement is valid or not.
|
||||
func (statement Statement) isValid() error {
|
||||
if !statement.Effect.IsValid() {
|
||||
return Errorf("invalid Effect %v", statement.Effect)
|
||||
}
|
||||
|
||||
if !statement.Principal.IsValid() {
|
||||
return Errorf("invalid Principal %v", statement.Principal)
|
||||
}
|
||||
|
||||
if len(statement.Actions) == 0 {
|
||||
return Errorf("Action must not be empty")
|
||||
}
|
||||
|
||||
if len(statement.Resources) == 0 {
|
||||
return Errorf("Resource must not be empty")
|
||||
}
|
||||
|
||||
for action := range statement.Actions {
|
||||
if action.isObjectAction() {
|
||||
if !statement.Resources.objectResourceExists() {
|
||||
return Errorf("unsupported Resource found %v for action %v", statement.Resources, action)
|
||||
}
|
||||
} else {
|
||||
if !statement.Resources.bucketResourceExists() {
|
||||
return Errorf("unsupported Resource found %v for action %v", statement.Resources, action)
|
||||
}
|
||||
}
|
||||
|
||||
keys := statement.Conditions.Keys()
|
||||
keyDiff := keys.Difference(actionConditionKeyMap[action])
|
||||
if !keyDiff.IsEmpty() {
|
||||
return Errorf("unsupported condition keys '%v' used for action '%v'", keyDiff, action)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON - encodes JSON data to Statement.
|
||||
func (statement Statement) MarshalJSON() ([]byte, error) {
|
||||
if err := statement.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// subtype to avoid recursive call to MarshalJSON()
|
||||
type subStatement Statement
|
||||
ss := subStatement(statement)
|
||||
return json.Marshal(ss)
|
||||
}
|
||||
|
||||
// UnmarshalJSON - decodes JSON data to Statement.
|
||||
func (statement *Statement) UnmarshalJSON(data []byte) error {
|
||||
// subtype to avoid recursive call to UnmarshalJSON()
|
||||
type subStatement Statement
|
||||
var ss subStatement
|
||||
|
||||
if err := json.Unmarshal(data, &ss); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s := Statement(ss)
|
||||
if err := s.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*statement = s
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate - validates Statement is for given bucket or not.
|
||||
func (statement Statement) Validate(bucketName string) error {
|
||||
if err := statement.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return statement.Resources.Validate(bucketName)
|
||||
}
|
||||
|
||||
// NewStatement - creates new statement.
|
||||
func NewStatement(effect Effect, principal Principal, actionSet ActionSet, resourceSet ResourceSet, conditions condition.Functions) Statement {
|
||||
return Statement{
|
||||
Effect: effect,
|
||||
Principal: principal,
|
||||
Actions: actionSet,
|
||||
Resources: resourceSet,
|
||||
Conditions: conditions,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 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 policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/bucket/policy/condition"
|
||||
)
|
||||
|
||||
func TestStatementIsAllowed(t *testing.T) {
|
||||
case1Statement := NewStatement(
|
||||
Allow,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(GetBucketLocationAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("*", "")),
|
||||
condition.NewFunctions(),
|
||||
)
|
||||
|
||||
case2Statement := NewStatement(
|
||||
Allow,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(GetObjectAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
condition.NewFunctions(),
|
||||
)
|
||||
|
||||
_, IPNet1, err := net.ParseCIDR("192.168.1.0/24")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
func1, err := condition.NewIPAddressFunc(
|
||||
condition.AWSSourceIP,
|
||||
IPNet1,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
case3Statement := NewStatement(
|
||||
Allow,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(GetObjectAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
condition.NewFunctions(func1),
|
||||
)
|
||||
|
||||
case4Statement := NewStatement(
|
||||
Deny,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(GetObjectAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
condition.NewFunctions(func1),
|
||||
)
|
||||
|
||||
anonGetBucketLocationArgs := Args{
|
||||
AccountName: "Q3AM3UQ867SPQQA43P2F",
|
||||
Action: GetBucketLocationAction,
|
||||
BucketName: "mybucket",
|
||||
ConditionValues: map[string][]string{},
|
||||
}
|
||||
|
||||
anonPutObjectActionArgs := Args{
|
||||
AccountName: "Q3AM3UQ867SPQQA43P2F",
|
||||
Action: PutObjectAction,
|
||||
BucketName: "mybucket",
|
||||
ConditionValues: map[string][]string{
|
||||
"x-amz-copy-source": {"mybucket/myobject"},
|
||||
"SourceIp": {"192.168.1.10"},
|
||||
},
|
||||
ObjectName: "myobject",
|
||||
}
|
||||
|
||||
anonGetObjectActionArgs := Args{
|
||||
AccountName: "Q3AM3UQ867SPQQA43P2F",
|
||||
Action: GetObjectAction,
|
||||
BucketName: "mybucket",
|
||||
ConditionValues: map[string][]string{},
|
||||
ObjectName: "myobject",
|
||||
}
|
||||
|
||||
getBucketLocationArgs := Args{
|
||||
AccountName: "Q3AM3UQ867SPQQA43P2F",
|
||||
Action: GetBucketLocationAction,
|
||||
BucketName: "mybucket",
|
||||
ConditionValues: map[string][]string{},
|
||||
IsOwner: true,
|
||||
}
|
||||
|
||||
putObjectActionArgs := Args{
|
||||
AccountName: "Q3AM3UQ867SPQQA43P2F",
|
||||
Action: PutObjectAction,
|
||||
BucketName: "mybucket",
|
||||
ConditionValues: map[string][]string{
|
||||
"x-amz-copy-source": {"mybucket/myobject"},
|
||||
"SourceIp": {"192.168.1.10"},
|
||||
},
|
||||
IsOwner: true,
|
||||
ObjectName: "myobject",
|
||||
}
|
||||
|
||||
getObjectActionArgs := Args{
|
||||
AccountName: "Q3AM3UQ867SPQQA43P2F",
|
||||
Action: GetObjectAction,
|
||||
BucketName: "mybucket",
|
||||
ConditionValues: map[string][]string{},
|
||||
IsOwner: true,
|
||||
ObjectName: "myobject",
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
statement Statement
|
||||
args Args
|
||||
expectedResult bool
|
||||
}{
|
||||
{case1Statement, anonGetBucketLocationArgs, true},
|
||||
{case1Statement, anonPutObjectActionArgs, true},
|
||||
{case1Statement, anonGetObjectActionArgs, false},
|
||||
{case1Statement, getBucketLocationArgs, true},
|
||||
{case1Statement, putObjectActionArgs, true},
|
||||
{case1Statement, getObjectActionArgs, false},
|
||||
|
||||
{case2Statement, anonGetBucketLocationArgs, false},
|
||||
{case2Statement, anonPutObjectActionArgs, true},
|
||||
{case2Statement, anonGetObjectActionArgs, true},
|
||||
{case2Statement, getBucketLocationArgs, false},
|
||||
{case2Statement, putObjectActionArgs, true},
|
||||
{case2Statement, getObjectActionArgs, true},
|
||||
|
||||
{case3Statement, anonGetBucketLocationArgs, false},
|
||||
{case3Statement, anonPutObjectActionArgs, true},
|
||||
{case3Statement, anonGetObjectActionArgs, false},
|
||||
{case3Statement, getBucketLocationArgs, false},
|
||||
{case3Statement, putObjectActionArgs, true},
|
||||
{case3Statement, getObjectActionArgs, false},
|
||||
|
||||
{case4Statement, anonGetBucketLocationArgs, true},
|
||||
{case4Statement, anonPutObjectActionArgs, false},
|
||||
{case4Statement, anonGetObjectActionArgs, true},
|
||||
{case4Statement, getBucketLocationArgs, true},
|
||||
{case4Statement, putObjectActionArgs, false},
|
||||
{case4Statement, getObjectActionArgs, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.statement.IsAllowed(testCase.args)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatementIsValid(t *testing.T) {
|
||||
_, IPNet1, err := net.ParseCIDR("192.168.1.0/24")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
func1, err := condition.NewIPAddressFunc(
|
||||
condition.AWSSourceIP,
|
||||
IPNet1,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
func2, err := condition.NewStringEqualsFunc(
|
||||
condition.S3XAmzCopySource,
|
||||
"mybucket/myobject",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
statement Statement
|
||||
expectErr bool
|
||||
}{
|
||||
// Invalid effect error.
|
||||
{NewStatement(
|
||||
Effect("foo"),
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(GetBucketLocationAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("*", "")),
|
||||
condition.NewFunctions(),
|
||||
), true},
|
||||
// Invalid principal error.
|
||||
{NewStatement(
|
||||
Allow,
|
||||
NewPrincipal(),
|
||||
NewActionSet(GetBucketLocationAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("*", "")),
|
||||
condition.NewFunctions(),
|
||||
), true},
|
||||
// Empty actions error.
|
||||
{NewStatement(
|
||||
Allow,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(),
|
||||
NewResourceSet(NewResource("*", "")),
|
||||
condition.NewFunctions(),
|
||||
), true},
|
||||
// Empty resources error.
|
||||
{NewStatement(
|
||||
Allow,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(GetBucketLocationAction, PutObjectAction),
|
||||
NewResourceSet(),
|
||||
condition.NewFunctions(),
|
||||
), true},
|
||||
// Unsupported resource found for object action.
|
||||
{NewStatement(
|
||||
Allow,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(GetBucketLocationAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "")),
|
||||
condition.NewFunctions(),
|
||||
), true},
|
||||
// Unsupported resource found for bucket action.
|
||||
{NewStatement(
|
||||
Allow,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(GetBucketLocationAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "myobject*")),
|
||||
condition.NewFunctions(),
|
||||
), true},
|
||||
// Unsupported condition key for action.
|
||||
{NewStatement(
|
||||
Allow,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(GetObjectAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "myobject*")),
|
||||
condition.NewFunctions(func1, func2),
|
||||
), true},
|
||||
{NewStatement(
|
||||
Deny,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(GetObjectAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "myobject*")),
|
||||
condition.NewFunctions(func1),
|
||||
), false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := testCase.statement.isValid()
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatementMarshalJSON(t *testing.T) {
|
||||
case1Statement := NewStatement(
|
||||
Allow,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
condition.NewFunctions(),
|
||||
)
|
||||
case1Statement.SID = "SomeId1"
|
||||
case1Data := []byte(`{"Sid":"SomeId1","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"]}`)
|
||||
|
||||
func1, err := condition.NewNullFunc(
|
||||
condition.S3XAmzCopySource,
|
||||
true,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
case2Statement := NewStatement(
|
||||
Allow,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
condition.NewFunctions(func1),
|
||||
)
|
||||
case2Data := []byte(`{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"],"Condition":{"Null":{"s3:x-amz-copy-source":[true]}}}`)
|
||||
|
||||
func2, err := condition.NewNullFunc(
|
||||
condition.S3XAmzServerSideEncryption,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
case3Statement := NewStatement(
|
||||
Deny,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
condition.NewFunctions(func2),
|
||||
)
|
||||
case3Data := []byte(`{"Effect":"Deny","Principal":{"AWS":["*"]},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"],"Condition":{"Null":{"s3:x-amz-server-side-encryption":[false]}}}`)
|
||||
|
||||
case4Statement := NewStatement(
|
||||
Allow,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(GetObjectAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "myobject*")),
|
||||
condition.NewFunctions(func1, func2),
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
statement Statement
|
||||
expectedResult []byte
|
||||
expectErr bool
|
||||
}{
|
||||
{case1Statement, case1Data, false},
|
||||
{case2Statement, case2Data, false},
|
||||
{case3Statement, case3Data, false},
|
||||
// Invalid statement error.
|
||||
{case4Statement, nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := json.Marshal(testCase.statement)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatementUnmarshalJSON(t *testing.T) {
|
||||
case1Data := []byte(`{
|
||||
"Sid": "SomeId1",
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": "s3:PutObject",
|
||||
"Resource": "arn:aws:s3:::mybucket/myobject*"
|
||||
}`)
|
||||
case1Statement := NewStatement(
|
||||
Allow,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
condition.NewFunctions(),
|
||||
)
|
||||
case1Statement.SID = "SomeId1"
|
||||
|
||||
case2Data := []byte(`{
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": "s3:PutObject",
|
||||
"Resource": "arn:aws:s3:::mybucket/myobject*",
|
||||
"Condition": {
|
||||
"Null": {
|
||||
"s3:x-amz-copy-source": true
|
||||
}
|
||||
}
|
||||
}`)
|
||||
func1, err := condition.NewNullFunc(
|
||||
condition.S3XAmzCopySource,
|
||||
true,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
case2Statement := NewStatement(
|
||||
Allow,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
condition.NewFunctions(func1),
|
||||
)
|
||||
|
||||
case3Data := []byte(`{
|
||||
"Effect": "Deny",
|
||||
"Principal": {
|
||||
"AWS": "*"
|
||||
},
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:GetObject"
|
||||
],
|
||||
"Resource": "arn:aws:s3:::mybucket/myobject*",
|
||||
"Condition": {
|
||||
"Null": {
|
||||
"s3:x-amz-server-side-encryption": "false"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
func2, err := condition.NewNullFunc(
|
||||
condition.S3XAmzServerSideEncryption,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
case3Statement := NewStatement(
|
||||
Deny,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(PutObjectAction, GetObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
condition.NewFunctions(func2),
|
||||
)
|
||||
|
||||
case4Data := []byte(`{
|
||||
"Effect": "Allow",
|
||||
"Principal": "Q3AM3UQ867SPQQA43P2F",
|
||||
"Action": "s3:PutObject",
|
||||
"Resource": "arn:aws:s3:::mybucket/myobject*"
|
||||
}`)
|
||||
|
||||
case5Data := []byte(`{
|
||||
"Principal": "*",
|
||||
"Action": "s3:PutObject",
|
||||
"Resource": "arn:aws:s3:::mybucket/myobject*"
|
||||
}`)
|
||||
|
||||
case6Data := []byte(`{
|
||||
"Effect": "Allow",
|
||||
"Action": "s3:PutObject",
|
||||
"Resource": "arn:aws:s3:::mybucket/myobject*"
|
||||
}`)
|
||||
|
||||
case7Data := []byte(`{
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Resource": "arn:aws:s3:::mybucket/myobject*"
|
||||
}`)
|
||||
|
||||
case8Data := []byte(`{
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": "s3:PutObject"
|
||||
}`)
|
||||
|
||||
case9Data := []byte(`{
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": "s3:PutObject",
|
||||
"Resource": "arn:aws:s3:::mybucket/myobject*",
|
||||
"Condition": {
|
||||
}
|
||||
}`)
|
||||
|
||||
case10Data := []byte(`{
|
||||
"Effect": "Deny",
|
||||
"Principal": {
|
||||
"AWS": "*"
|
||||
},
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:GetObject"
|
||||
],
|
||||
"Resource": "arn:aws:s3:::mybucket/myobject*",
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"s3:x-amz-copy-source": "yourbucket/myobject*"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
testCases := []struct {
|
||||
data []byte
|
||||
expectedResult Statement
|
||||
expectErr bool
|
||||
}{
|
||||
{case1Data, case1Statement, false},
|
||||
{case2Data, case2Statement, false},
|
||||
{case3Data, case3Statement, false},
|
||||
// JSON unmarshaling error.
|
||||
{case4Data, Statement{}, true},
|
||||
// Invalid effect error.
|
||||
{case5Data, Statement{}, true},
|
||||
// empty principal error.
|
||||
{case6Data, Statement{}, true},
|
||||
// Empty action error.
|
||||
{case7Data, Statement{}, true},
|
||||
// Empty resource error.
|
||||
{case8Data, Statement{}, true},
|
||||
// Empty condition error.
|
||||
{case9Data, Statement{}, true},
|
||||
// Unsupported condition key error.
|
||||
{case10Data, Statement{}, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
var result Statement
|
||||
err := json.Unmarshal(testCase.data, &result)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatementValidate(t *testing.T) {
|
||||
case1Statement := NewStatement(
|
||||
Allow,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
condition.NewFunctions(),
|
||||
)
|
||||
|
||||
func1, err := condition.NewNullFunc(
|
||||
condition.S3XAmzCopySource,
|
||||
true,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
func2, err := condition.NewNullFunc(
|
||||
condition.S3XAmzServerSideEncryption,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error. %v\n", err)
|
||||
}
|
||||
case2Statement := NewStatement(
|
||||
Allow,
|
||||
NewPrincipal("*"),
|
||||
NewActionSet(GetObjectAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "myobject*")),
|
||||
condition.NewFunctions(func1, func2),
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
statement Statement
|
||||
bucketName string
|
||||
expectErr bool
|
||||
}{
|
||||
{case1Statement, "mybucket", false},
|
||||
{case2Statement, "mybucket", true},
|
||||
{case1Statement, "yourbucket", true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := testCase.statement.Validate(testCase.bucketName)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user