mirror of
https://github.com/minio/minio.git
synced 2025-11-10 14:09:48 -05:00
Introduce STS client grants API and OPA policy integration (#6168)
This PR introduces two new features - AWS STS compatible STS API named AssumeRoleWithClientGrants ``` POST /?Action=AssumeRoleWithClientGrants&Token=<jwt> ``` This API endpoint returns temporary access credentials, access tokens signature types supported by this API - RSA keys - ECDSA keys Fetches the required public key from the JWKS endpoints, provides them as rsa or ecdsa public keys. - External policy engine support, in this case OPA policy engine - Credentials are stored on disks
This commit is contained in:
committed by
kannappanr
parent
16a100b597
commit
54ae364def
272
pkg/iam/policy/action.go
Normal file
272
pkg/iam/policy/action.go
Normal file
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* 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 iampolicy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/minio/minio/pkg/policy/condition"
|
||||
"github.com/minio/minio/pkg/wildcard"
|
||||
)
|
||||
|
||||
// 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"
|
||||
|
||||
// AllActions - all API actions
|
||||
AllActions = "s3:*"
|
||||
)
|
||||
|
||||
// List of all supported actions.
|
||||
var supportedActions = map[Action]struct{}{
|
||||
AllActions: {},
|
||||
AbortMultipartUploadAction: {},
|
||||
CreateBucketAction: {},
|
||||
DeleteBucketAction: {},
|
||||
DeleteBucketPolicyAction: {},
|
||||
DeleteObjectAction: {},
|
||||
GetBucketLocationAction: {},
|
||||
GetBucketNotificationAction: {},
|
||||
GetBucketPolicyAction: {},
|
||||
GetObjectAction: {},
|
||||
HeadBucketAction: {},
|
||||
ListAllMyBucketsAction: {},
|
||||
ListBucketAction: {},
|
||||
ListBucketMultipartUploadsAction: {},
|
||||
ListenBucketNotificationAction: {},
|
||||
ListMultipartUploadPartsAction: {},
|
||||
PutBucketNotificationAction: {},
|
||||
PutBucketPolicyAction: {},
|
||||
PutObjectAction: {},
|
||||
}
|
||||
|
||||
// isObjectAction - returns whether action is object type or not.
|
||||
func (action Action) isObjectAction() bool {
|
||||
switch action {
|
||||
case AbortMultipartUploadAction, DeleteObjectAction, GetObjectAction:
|
||||
fallthrough
|
||||
case ListMultipartUploadPartsAction, PutObjectAction, AllActions:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Match - matches object name with resource pattern.
|
||||
func (action Action) Match(a Action) bool {
|
||||
return wildcard.Match(string(action), string(a))
|
||||
}
|
||||
|
||||
// IsValid - checks if action is valid or not.
|
||||
func (action Action) IsValid() bool {
|
||||
_, ok := supportedActions[action]
|
||||
return ok
|
||||
}
|
||||
|
||||
// MarshalJSON - encodes Action to JSON data.
|
||||
func (action Action) MarshalJSON() ([]byte, error) {
|
||||
if action.IsValid() {
|
||||
return json.Marshal(string(action))
|
||||
}
|
||||
|
||||
return nil, fmt.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 fmt.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, fmt.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.AWSReferer,
|
||||
condition.AWSSourceIP,
|
||||
),
|
||||
|
||||
CreateBucketAction: condition.NewKeySet(
|
||||
condition.AWSReferer,
|
||||
condition.AWSSourceIP,
|
||||
),
|
||||
|
||||
DeleteBucketPolicyAction: condition.NewKeySet(
|
||||
condition.AWSReferer,
|
||||
condition.AWSSourceIP,
|
||||
),
|
||||
|
||||
DeleteObjectAction: condition.NewKeySet(
|
||||
condition.AWSReferer,
|
||||
condition.AWSSourceIP,
|
||||
),
|
||||
|
||||
GetBucketLocationAction: condition.NewKeySet(
|
||||
condition.AWSReferer,
|
||||
condition.AWSSourceIP,
|
||||
),
|
||||
|
||||
GetBucketNotificationAction: condition.NewKeySet(
|
||||
condition.AWSReferer,
|
||||
condition.AWSSourceIP,
|
||||
),
|
||||
|
||||
GetBucketPolicyAction: condition.NewKeySet(
|
||||
condition.AWSReferer,
|
||||
condition.AWSSourceIP,
|
||||
),
|
||||
|
||||
GetObjectAction: condition.NewKeySet(
|
||||
condition.S3XAmzServerSideEncryption,
|
||||
condition.S3XAmzServerSideEncryptionAwsKMSKeyID,
|
||||
condition.S3XAmzStorageClass,
|
||||
condition.AWSReferer,
|
||||
condition.AWSSourceIP,
|
||||
),
|
||||
|
||||
HeadBucketAction: condition.NewKeySet(
|
||||
condition.AWSReferer,
|
||||
condition.AWSSourceIP,
|
||||
),
|
||||
|
||||
ListAllMyBucketsAction: condition.NewKeySet(
|
||||
condition.AWSReferer,
|
||||
condition.AWSSourceIP,
|
||||
),
|
||||
|
||||
ListBucketAction: condition.NewKeySet(
|
||||
condition.S3Prefix,
|
||||
condition.S3Delimiter,
|
||||
condition.S3MaxKeys,
|
||||
condition.AWSReferer,
|
||||
condition.AWSSourceIP,
|
||||
),
|
||||
|
||||
ListBucketMultipartUploadsAction: condition.NewKeySet(
|
||||
condition.AWSReferer,
|
||||
condition.AWSSourceIP,
|
||||
),
|
||||
|
||||
ListenBucketNotificationAction: condition.NewKeySet(
|
||||
condition.AWSReferer,
|
||||
condition.AWSSourceIP,
|
||||
),
|
||||
|
||||
ListMultipartUploadPartsAction: condition.NewKeySet(
|
||||
condition.AWSReferer,
|
||||
condition.AWSSourceIP,
|
||||
),
|
||||
|
||||
PutBucketNotificationAction: condition.NewKeySet(
|
||||
condition.AWSReferer,
|
||||
condition.AWSSourceIP,
|
||||
),
|
||||
|
||||
PutBucketPolicyAction: condition.NewKeySet(
|
||||
condition.AWSReferer,
|
||||
condition.AWSSourceIP,
|
||||
),
|
||||
|
||||
PutObjectAction: condition.NewKeySet(
|
||||
condition.S3XAmzCopySource,
|
||||
condition.S3XAmzServerSideEncryption,
|
||||
condition.S3XAmzServerSideEncryptionAwsKMSKeyID,
|
||||
condition.S3XAmzMetadataDirective,
|
||||
condition.S3XAmzStorageClass,
|
||||
condition.AWSReferer,
|
||||
condition.AWSSourceIP,
|
||||
),
|
||||
}
|
||||
116
pkg/iam/policy/action_test.go
Normal file
116
pkg/iam/policy/action_test.go
Normal 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 iampolicy
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
119
pkg/iam/policy/actionset.go
Normal file
119
pkg/iam/policy/actionset.go
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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 iampolicy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/minio/minio-go/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{}{}
|
||||
}
|
||||
|
||||
// Match - matches object name with anyone of action pattern in action set.
|
||||
func (actionSet ActionSet) Match(action Action) bool {
|
||||
for r := range actionSet {
|
||||
if r.Match(action) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 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, fmt.Errorf("empty action set")
|
||||
}
|
||||
|
||||
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 fmt.Errorf("empty action set")
|
||||
}
|
||||
|
||||
*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
|
||||
}
|
||||
159
pkg/iam/policy/actionset_test.go
Normal file
159
pkg/iam/policy/actionset_test.go
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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 iampolicy
|
||||
|
||||
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 TestActionSetMatches(t *testing.T) {
|
||||
testCases := []struct {
|
||||
set ActionSet
|
||||
action Action
|
||||
expectedResult bool
|
||||
}{
|
||||
{NewActionSet(AllActions), AbortMultipartUploadAction, true},
|
||||
{NewActionSet(PutObjectAction), PutObjectAction, true},
|
||||
{NewActionSet(PutObjectAction, GetObjectAction), PutObjectAction, true},
|
||||
{NewActionSet(PutObjectAction, GetObjectAction), AbortMultipartUploadAction, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.set.Match(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
172
pkg/iam/policy/opa.go
Normal file
172
pkg/iam/policy/opa.go
Normal file
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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 iampolicy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
|
||||
// OpaArgs opa general purpose policy engine configuration.
|
||||
type OpaArgs struct {
|
||||
URL *xnet.URL `json:"url"`
|
||||
AuthToken string `json:"authToken"`
|
||||
}
|
||||
|
||||
// Validate - validate opa configuration params.
|
||||
func (a *OpaArgs) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON - decodes JSON data.
|
||||
func (a *OpaArgs) UnmarshalJSON(data []byte) error {
|
||||
// subtype to avoid recursive call to UnmarshalJSON()
|
||||
type subOpaArgs OpaArgs
|
||||
var so subOpaArgs
|
||||
|
||||
if opaURL, ok := os.LookupEnv("MINIO_IAM_OPA_URL"); ok {
|
||||
u, err := xnet.ParseURL(opaURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
so.URL = u
|
||||
so.AuthToken = os.Getenv("MINIO_IAM_OPA_AUTHTOKEN")
|
||||
} else {
|
||||
if err := json.Unmarshal(data, &so); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
oa := OpaArgs(so)
|
||||
if oa.URL == nil || oa.URL.String() == "" {
|
||||
*a = oa
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := oa.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*a = oa
|
||||
return nil
|
||||
}
|
||||
|
||||
// Opa - implements opa policy agent calls.
|
||||
type Opa struct {
|
||||
args OpaArgs
|
||||
secureFailed bool
|
||||
client *http.Client
|
||||
insecureClient *http.Client
|
||||
}
|
||||
|
||||
// newCustomHTTPTransport returns a new http configuration
|
||||
// used while communicating with the cloud backends.
|
||||
// This sets the value for MaxIdleConnsPerHost from 2 (go default)
|
||||
// to 100.
|
||||
func newCustomHTTPTransport(insecure bool) *http.Transport {
|
||||
return &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 1024,
|
||||
MaxIdleConnsPerHost: 1024,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
|
||||
DisableCompression: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewOpa - initializes opa policy engine connector.
|
||||
func NewOpa(args OpaArgs) *Opa {
|
||||
// No opa args.
|
||||
if args.URL == nil && args.AuthToken == "" {
|
||||
return nil
|
||||
}
|
||||
return &Opa{
|
||||
args: args,
|
||||
client: &http.Client{Transport: newCustomHTTPTransport(false)},
|
||||
insecureClient: &http.Client{Transport: newCustomHTTPTransport(true)},
|
||||
}
|
||||
}
|
||||
|
||||
// IsAllowed - checks given policy args is allowed to continue the REST API.
|
||||
func (o *Opa) IsAllowed(args Args) bool {
|
||||
if o == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// OPA input
|
||||
body := make(map[string]interface{})
|
||||
body["input"] = args
|
||||
|
||||
inputBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", o.args.URL.String(), bytes.NewReader(inputBytes))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if o.args.AuthToken != "" {
|
||||
req.Header.Set("Authorization", o.args.AuthToken)
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
if o.secureFailed {
|
||||
resp, err = o.insecureClient.Do(req)
|
||||
} else {
|
||||
resp, err = o.client.Do(req)
|
||||
if err != nil {
|
||||
o.secureFailed = true
|
||||
resp, err = o.insecureClient.Do(req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Handle OPA response
|
||||
type opaResponse struct {
|
||||
Result struct {
|
||||
Allow bool `json:"allow"`
|
||||
} `json:"result"`
|
||||
}
|
||||
var result opaResponse
|
||||
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return result.Result.Allow
|
||||
}
|
||||
173
pkg/iam/policy/policy.go
Normal file
173
pkg/iam/policy/policy.go
Normal file
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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 iampolicy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
Claims map[string]interface{} `json:"claims"`
|
||||
}
|
||||
|
||||
// Policy - iam bucket iamp.
|
||||
type Policy struct {
|
||||
ID policy.ID `json:"ID,omitempty"`
|
||||
Version string
|
||||
Statements []Statement `json:"Statement"`
|
||||
}
|
||||
|
||||
// IsAllowed - checks given policy args is allowed to continue the Rest API.
|
||||
func (iamp Policy) IsAllowed(args Args) bool {
|
||||
// Check all deny statements. If any one statement denies, return false.
|
||||
for _, statement := range iamp.Statements {
|
||||
if statement.Effect == policy.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 iamp.Statements {
|
||||
if statement.Effect == policy.Allow {
|
||||
if statement.IsAllowed(args) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsEmpty - returns whether policy is empty or not.
|
||||
func (iamp Policy) IsEmpty() bool {
|
||||
return len(iamp.Statements) == 0
|
||||
}
|
||||
|
||||
// isValid - checks if Policy is valid or not.
|
||||
func (iamp Policy) isValid() error {
|
||||
if iamp.Version != DefaultVersion && iamp.Version != "" {
|
||||
return fmt.Errorf("invalid version '%v'", iamp.Version)
|
||||
}
|
||||
|
||||
for _, statement := range iamp.Statements {
|
||||
if err := statement.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for i := range iamp.Statements {
|
||||
for _, statement := range iamp.Statements[i+1:] {
|
||||
actions := iamp.Statements[i].Actions.Intersection(statement.Actions)
|
||||
if len(actions) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
resources := iamp.Statements[i].Resources.Intersection(statement.Resources)
|
||||
if len(resources) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if iamp.Statements[i].Conditions.String() != statement.Conditions.String() {
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Errorf("duplicate actions %v, resources %v found in statements %v, %v",
|
||||
actions, resources, iamp.Statements[i], statement)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON - encodes Policy to JSON data.
|
||||
func (iamp Policy) MarshalJSON() ([]byte, error) {
|
||||
if err := iamp.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// subtype to avoid recursive call to MarshalJSON()
|
||||
type subPolicy Policy
|
||||
return json.Marshal(subPolicy(iamp))
|
||||
}
|
||||
|
||||
// UnmarshalJSON - decodes JSON data to Iamp.
|
||||
func (iamp *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
|
||||
}
|
||||
|
||||
*iamp = p
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate - validates all statements are for given bucket or not.
|
||||
func (iamp Policy) Validate() error {
|
||||
if err := iamp.isValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, statement := range iamp.Statements {
|
||||
if err := statement.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseConfig - parses data in given reader to Iamp.
|
||||
func ParseConfig(reader io.Reader) (*Policy, error) {
|
||||
var iamp Policy
|
||||
|
||||
decoder := json.NewDecoder(reader)
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&iamp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &iamp, iamp.Validate()
|
||||
}
|
||||
1032
pkg/iam/policy/policy_test.go
Normal file
1032
pkg/iam/policy/policy_test.go
Normal file
File diff suppressed because it is too large
Load Diff
129
pkg/iam/policy/resource.go
Normal file
129
pkg/iam/policy/resource.go
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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 iampolicy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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, "/") || r.Pattern == "*"
|
||||
}
|
||||
|
||||
func (r Resource) isObjectPattern() bool {
|
||||
return strings.Contains(r.Pattern, "/") || strings.Contains(r.BucketName, "*") || r.Pattern == "*/*"
|
||||
}
|
||||
|
||||
// IsValid - checks whether Resource is valid or not.
|
||||
func (r Resource) IsValid() bool {
|
||||
return r.Pattern != ""
|
||||
}
|
||||
|
||||
// Match - matches object name with resource pattern.
|
||||
func (r Resource) Match(resource string) bool {
|
||||
if strings.HasPrefix(resource, r.Pattern) {
|
||||
return true
|
||||
}
|
||||
return wildcard.Match(r.Pattern, resource)
|
||||
}
|
||||
|
||||
// MarshalJSON - encodes Resource to JSON data.
|
||||
func (r Resource) MarshalJSON() ([]byte, error) {
|
||||
if !r.IsValid() {
|
||||
return nil, fmt.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() error {
|
||||
if !r.IsValid() {
|
||||
return fmt.Errorf("invalid resource")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseResource - parses string to Resource.
|
||||
func parseResource(s string) (Resource, error) {
|
||||
if !strings.HasPrefix(s, ResourceARNPrefix) {
|
||||
return Resource{}, fmt.Errorf("invalid resource '%v'", s)
|
||||
}
|
||||
|
||||
pattern := strings.TrimPrefix(s, ResourceARNPrefix)
|
||||
tokens := strings.SplitN(pattern, "/", 2)
|
||||
bucketName := tokens[0]
|
||||
if bucketName == "" {
|
||||
return Resource{}, fmt.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,
|
||||
}
|
||||
}
|
||||
220
pkg/iam/policy/resource_test.go
Normal file
220
pkg/iam/policy/resource_test.go
Normal file
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* 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 iampolicy
|
||||
|
||||
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("", "*"), true},
|
||||
{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", true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.resource.Match(testCase.objectName)
|
||||
|
||||
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("", "*"), []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},
|
||||
}
|
||||
|
||||
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
|
||||
expectErr bool
|
||||
}{
|
||||
{NewResource("mybucket", "/myobject*"), false},
|
||||
{NewResource("", "/myobject*"), false},
|
||||
{NewResource("", ""), true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := testCase.resource.Validate()
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
147
pkg/iam/policy/resourceset.go
Normal file
147
pkg/iam/policy/resourceset.go
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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 iampolicy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/minio/minio-go/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{}{}
|
||||
}
|
||||
|
||||
// Intersection - returns resources available in both ResourceSet.
|
||||
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, fmt.Errorf("empty resource set")
|
||||
}
|
||||
|
||||
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) bool {
|
||||
for r := range resourceSet {
|
||||
if r.Match(resource) {
|
||||
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 fmt.Errorf("duplicate resource '%v' found", s)
|
||||
}
|
||||
|
||||
resourceSet.Add(resource)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate - validates ResourceSet.
|
||||
func (resourceSet ResourceSet) Validate() error {
|
||||
for resource := range resourceSet {
|
||||
if err := resource.Validate(); 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
|
||||
}
|
||||
239
pkg/iam/policy/resourceset_test.go
Normal file
239
pkg/iam/policy/resourceset_test.go
Normal file
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* 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 iampolicy
|
||||
|
||||
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", true},
|
||||
{NewResourceSet(), "mybucket/myobject", false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.resourceSet.Match(testCase.resource)
|
||||
|
||||
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
|
||||
expectErr bool
|
||||
}{
|
||||
{NewResourceSet(NewResource("mybucket", "/myobject*")), false},
|
||||
{NewResourceSet(NewResource("", "/myobject*")), false},
|
||||
{NewResourceSet(NewResource("", "")), true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := testCase.resourceSet.Validate()
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
141
pkg/iam/policy/statement.go
Normal file
141
pkg/iam/policy/statement.go
Normal 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 iampolicy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/policy/condition"
|
||||
)
|
||||
|
||||
// Statement - iam policy statement.
|
||||
type Statement struct {
|
||||
SID policy.ID `json:"Sid,omitempty"`
|
||||
Effect policy.Effect `json:"Effect"`
|
||||
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.Actions.Match(args.Action) {
|
||||
return false
|
||||
}
|
||||
|
||||
resource := args.BucketName
|
||||
if args.ObjectName != "" {
|
||||
if !strings.HasPrefix(args.ObjectName, "/") {
|
||||
resource += "/"
|
||||
}
|
||||
|
||||
resource += args.ObjectName
|
||||
}
|
||||
|
||||
if !statement.Resources.Match(resource) {
|
||||
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 fmt.Errorf("invalid Effect %v", statement.Effect)
|
||||
}
|
||||
|
||||
if len(statement.Actions) == 0 {
|
||||
return fmt.Errorf("Action must not be empty")
|
||||
}
|
||||
|
||||
if len(statement.Resources) == 0 {
|
||||
return fmt.Errorf("Resource must not be empty")
|
||||
}
|
||||
|
||||
if err := statement.Resources.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for action := range statement.Actions {
|
||||
if !statement.Resources.objectResourceExists() && !statement.Resources.bucketResourceExists() {
|
||||
return fmt.Errorf("unsupported Resource found %v for action %v", statement.Resources, action)
|
||||
}
|
||||
|
||||
keys := statement.Conditions.Keys()
|
||||
keyDiff := keys.Difference(actionConditionKeyMap[action])
|
||||
if !keyDiff.IsEmpty() {
|
||||
return fmt.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() error {
|
||||
return statement.isValid()
|
||||
}
|
||||
|
||||
// NewStatement - creates new statement.
|
||||
func NewStatement(effect policy.Effect, actionSet ActionSet, resourceSet ResourceSet, conditions condition.Functions) Statement {
|
||||
return Statement{
|
||||
Effect: effect,
|
||||
Actions: actionSet,
|
||||
Resources: resourceSet,
|
||||
Conditions: conditions,
|
||||
}
|
||||
}
|
||||
515
pkg/iam/policy/statement_test.go
Normal file
515
pkg/iam/policy/statement_test.go
Normal file
@@ -0,0 +1,515 @@
|
||||
/*
|
||||
* 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 iampolicy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/policy/condition"
|
||||
)
|
||||
|
||||
func TestStatementIsAllowed(t *testing.T) {
|
||||
case1Statement := NewStatement(
|
||||
policy.Allow,
|
||||
NewActionSet(GetBucketLocationAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("*", "")),
|
||||
condition.NewFunctions(),
|
||||
)
|
||||
|
||||
case2Statement := NewStatement(
|
||||
policy.Allow,
|
||||
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(
|
||||
policy.Allow,
|
||||
NewActionSet(GetObjectAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
condition.NewFunctions(func1),
|
||||
)
|
||||
|
||||
case4Statement := NewStatement(
|
||||
policy.Deny,
|
||||
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{},
|
||||
}
|
||||
|
||||
putObjectActionArgs := Args{
|
||||
AccountName: "Q3AM3UQ867SPQQA43P2F",
|
||||
Action: PutObjectAction,
|
||||
BucketName: "mybucket",
|
||||
ConditionValues: map[string][]string{
|
||||
"x-amz-copy-source": {"mybucket/myobject"},
|
||||
"SourceIp": {"192.168.1.10"},
|
||||
},
|
||||
ObjectName: "myobject",
|
||||
}
|
||||
|
||||
getObjectActionArgs := Args{
|
||||
AccountName: "Q3AM3UQ867SPQQA43P2F",
|
||||
Action: GetObjectAction,
|
||||
BucketName: "mybucket",
|
||||
ConditionValues: map[string][]string{},
|
||||
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(
|
||||
policy.Effect("foo"),
|
||||
NewActionSet(GetBucketLocationAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("*", "")),
|
||||
condition.NewFunctions(),
|
||||
), true},
|
||||
// Empty actions error.
|
||||
{NewStatement(
|
||||
policy.Allow,
|
||||
NewActionSet(),
|
||||
NewResourceSet(NewResource("*", "")),
|
||||
condition.NewFunctions(),
|
||||
), true},
|
||||
// Empty resources error.
|
||||
{NewStatement(
|
||||
policy.Allow,
|
||||
NewActionSet(GetBucketLocationAction, PutObjectAction),
|
||||
NewResourceSet(),
|
||||
condition.NewFunctions(),
|
||||
), true},
|
||||
// Unsupported conditions for GetObject
|
||||
{NewStatement(
|
||||
policy.Allow,
|
||||
NewActionSet(GetObjectAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "myobject*")),
|
||||
condition.NewFunctions(func1, func2),
|
||||
), true},
|
||||
{NewStatement(
|
||||
policy.Allow,
|
||||
NewActionSet(GetBucketLocationAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "myobject*")),
|
||||
condition.NewFunctions(),
|
||||
), false},
|
||||
{NewStatement(
|
||||
policy.Allow,
|
||||
NewActionSet(GetBucketLocationAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "")),
|
||||
condition.NewFunctions(),
|
||||
), false},
|
||||
{NewStatement(
|
||||
policy.Deny,
|
||||
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(
|
||||
policy.Allow,
|
||||
NewActionSet(PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
condition.NewFunctions(),
|
||||
)
|
||||
case1Statement.SID = "SomeId1"
|
||||
case1Data := []byte(`{"Sid":"SomeId1","Effect":"Allow","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(
|
||||
policy.Allow,
|
||||
NewActionSet(PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
condition.NewFunctions(func1),
|
||||
)
|
||||
case2Data := []byte(`{"Effect":"Allow","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(
|
||||
policy.Deny,
|
||||
NewActionSet(GetObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
condition.NewFunctions(func2),
|
||||
)
|
||||
case3Data := []byte(`{"Effect":"Deny","Action":["s3:GetObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"],"Condition":{"Null":{"s3:x-amz-server-side-encryption":[false]}}}`)
|
||||
|
||||
case4Statement := NewStatement(
|
||||
policy.Allow,
|
||||
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",
|
||||
"Action": "s3:PutObject",
|
||||
"Resource": "arn:aws:s3:::mybucket/myobject*"
|
||||
}`)
|
||||
case1Statement := NewStatement(
|
||||
policy.Allow,
|
||||
NewActionSet(PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
condition.NewFunctions(),
|
||||
)
|
||||
case1Statement.SID = "SomeId1"
|
||||
|
||||
case2Data := []byte(`{
|
||||
"Effect": "Allow",
|
||||
"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(
|
||||
policy.Allow,
|
||||
NewActionSet(PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
condition.NewFunctions(func1),
|
||||
)
|
||||
|
||||
case3Data := []byte(`{
|
||||
"Effect": "Deny",
|
||||
"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(
|
||||
policy.Deny,
|
||||
NewActionSet(PutObjectAction, GetObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "/myobject*")),
|
||||
condition.NewFunctions(func2),
|
||||
)
|
||||
|
||||
case4Data := []byte(`{
|
||||
"Effect": "Allow",
|
||||
"Action": "s3:PutObjec",
|
||||
"Resource": "arn:aws:s3:::mybucket/myobject*"
|
||||
}`)
|
||||
|
||||
case5Data := []byte(`{
|
||||
"Action": "s3:PutObject",
|
||||
"Resource": "arn:aws:s3:::mybucket/myobject*"
|
||||
}`)
|
||||
|
||||
case7Data := []byte(`{
|
||||
"Effect": "Allow",
|
||||
"Resource": "arn:aws:s3:::mybucket/myobject*"
|
||||
}`)
|
||||
|
||||
case8Data := []byte(`{
|
||||
"Effect": "Allow",
|
||||
"Action": "s3:PutObject"
|
||||
}`)
|
||||
|
||||
case9Data := []byte(`{
|
||||
"Effect": "Allow",
|
||||
"Action": "s3:PutObject",
|
||||
"Resource": "arn:aws:s3:::mybucket/myobject*",
|
||||
"Condition": {
|
||||
}
|
||||
}`)
|
||||
|
||||
case10Data := []byte(`{
|
||||
"Effect": "Deny",
|
||||
"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 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
|
||||
expectErr := (json.Unmarshal(testCase.data, &result) != 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(
|
||||
policy.Allow,
|
||||
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(
|
||||
policy.Allow,
|
||||
NewActionSet(GetObjectAction, PutObjectAction),
|
||||
NewResourceSet(NewResource("mybucket", "myobject*")),
|
||||
condition.NewFunctions(func1, func2),
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
statement Statement
|
||||
expectErr bool
|
||||
}{
|
||||
{case1Statement, false},
|
||||
{case2Statement, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := testCase.statement.Validate()
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user