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:
Harshavardhana
2020-01-28 03:42:34 +05:30
committed by kannappanr
parent 4cb6ebcfa2
commit 0cbebf0f57
109 changed files with 102 additions and 101 deletions
+42
View File
@@ -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
}
+135
View File
@@ -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()
}
+105
View File
@@ -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)
}
})
}
}
+32
View File
@@ -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
}
+56
View File
@@ -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)
}
})
}
}
+130
View File
@@ -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
}
+230
View File
@@ -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)
}
})
}
}
+65
View File
@@ -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
}
+86
View File
@@ -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
}
+112
View File
@@ -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)
}
})
}
}
+42
View File
@@ -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
}
+43
View File
@@ -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
}
+521
View File
@@ -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
}
+567
View File
@@ -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)
}
}
}
+44
View File
@@ -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()
}
+62
View File
@@ -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
}
+113
View File
@@ -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
}
+41
View File
@@ -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
}
+260
View File
@@ -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...),
}
+116
View File
@@ -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)
}
}
}
}
+132
View File
@@ -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
}
+158
View File
@@ -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)
}
}
}
}
+105
View File
@@ -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)
}
}
}
}
+175
View File
@@ -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)
}
+360
View File
@@ -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 &notIPAddressFunc{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},
{&notIPAddressFunc{}, 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)
}
}
}
}
+285
View File
@@ -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.
}
+214
View File
@@ -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)
}
}
}
+99
View File
@@ -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)
}
+106
View File
@@ -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)
}
}
}
}
+106
View File
@@ -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)
}
}
}
}
+175
View File
@@ -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
}
+260
View File
@@ -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)
}
}
}
}
+85
View File
@@ -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)
}
}
}
}
+77
View File
@@ -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
}
+122
View File
@@ -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)
}
}
}
}
+44
View File
@@ -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()
}
+56
View File
@@ -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
}
+97
View File
@@ -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)
}
}
}
}
+178
View File
@@ -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
+96
View File
@@ -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...)}
}
+141
View File
@@ -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)
}
}
}
}
+139
View File
@@ -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,
}
}
+221
View File
@@ -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)
}
}
}
+165
View File
@@ -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
}
+240
View File
@@ -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)
}
}
}
+155
View File
@@ -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,
}
}
+571
View File
@@ -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)
}
}
}