fix: support IAM policy handling for wildcard actions (#11530)

This PR fixes

- allow 's3:versionid` as a valid conditional for
  Get,Put,Tags,Object locking APIs
- allow additional headers missing for object APIs
- allow wildcard based action matching
This commit is contained in:
Harshavardhana 2021-02-12 23:05:09 -08:00 committed by GitHub
parent 79b6a43467
commit a94a9c37fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 365 additions and 25 deletions

View File

@ -110,10 +110,9 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
// -- IAM APIs -- // -- IAM APIs --
// Add policy IAM // Add policy IAM
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.AddCannedPolicy)).Queries("name", "{name:.*}") adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-canned-policy").HandlerFunc(httpTraceAll(adminAPI.AddCannedPolicy)).Queries("name", "{name:.*}")
// Add user IAM // Add user IAM
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/accountinfo").HandlerFunc(httpTraceAll(adminAPI.AccountInfoHandler)) adminRouter.Methods(http.MethodGet).Path(adminVersion + "/accountinfo").HandlerFunc(httpTraceAll(adminAPI.AccountInfoHandler))
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}") adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}")

View File

@ -124,6 +124,7 @@ var AllSupportedKeys = append([]Key{
S3Prefix, S3Prefix,
S3Delimiter, S3Delimiter,
S3MaxKeys, S3MaxKeys,
S3VersionID,
S3ObjectLockRemainingRetentionDays, S3ObjectLockRemainingRetentionDays,
S3ObjectLockMode, S3ObjectLockMode,
S3ObjectLockLegalHold, S3ObjectLockLegalHold,
@ -143,6 +144,8 @@ var AllSupportedKeys = append([]Key{
// CommonKeys - is list of all common condition keys. // CommonKeys - is list of all common condition keys.
var CommonKeys = append([]Key{ var CommonKeys = append([]Key{
S3XAmzContentSha256,
S3LocationConstraint,
AWSReferer, AWSReferer,
AWSSourceIP, AWSSourceIP,
AWSUserAgent, AWSUserAgent,
@ -152,7 +155,6 @@ var CommonKeys = append([]Key{
AWSPrincipalType, AWSPrincipalType,
AWSUserID, AWSUserID,
AWSUsername, AWSUsername,
S3XAmzContentSha256,
LDAPUser, LDAPUser,
}, JWTKeys...) }, JWTKeys...)
@ -241,6 +243,13 @@ func (set KeySet) Add(key Key) {
set[key] = struct{}{} set[key] = struct{}{}
} }
// Merge merges two key sets, duplicates are overwritten
func (set KeySet) Merge(mset KeySet) {
for k, v := range mset {
set[k] = v
}
}
// Difference - returns a key set contains difference of two keys. // Difference - returns a key set contains difference of two keys.
// Example: // Example:
// keySet1 := ["one", "two", "three"] // keySet1 := ["one", "two", "three"]

View File

@ -266,23 +266,45 @@ var supportedObjectActions = map[Action]struct{}{
// isObjectAction - returns whether action is object type or not. // isObjectAction - returns whether action is object type or not.
func (action Action) isObjectAction() bool { func (action Action) isObjectAction() bool {
_, ok := supportedObjectActions[action] for supAction := range supportedObjectActions {
return ok if action.Match(supAction) {
return true
}
}
return false
} }
// Match - matches object name with resource pattern. // Match - matches action name with action patter.
func (action Action) Match(a Action) bool { func (action Action) Match(a Action) bool {
return wildcard.Match(string(action), string(a)) return wildcard.Match(string(action), string(a))
} }
// IsValid - checks if action is valid or not. // IsValid - checks if action is valid or not.
func (action Action) IsValid() bool { func (action Action) IsValid() bool {
_, ok := supportedActions[action] for supAction := range supportedActions {
return ok if action.Match(supAction) {
return true
}
}
return false
} }
// actionConditionKeyMap - holds mapping of supported condition key for an action. type actionConditionKeyMap map[Action]condition.KeySet
var actionConditionKeyMap = map[Action]condition.KeySet{
func (a actionConditionKeyMap) Lookup(action Action) (condition.KeySet, bool) {
var ckeysMerged = condition.KeySet{}
var found bool
for act, ckey := range a {
if action.Match(act) {
ckeysMerged.Merge(ckey)
found = true
}
}
return ckeysMerged, found
}
// iamActionConditionKeyMap - holds mapping of supported condition key for an action.
var iamActionConditionKeyMap = actionConditionKeyMap{
AllActions: condition.NewKeySet(condition.AllSupportedKeys...), AllActions: condition.NewKeySet(condition.AllSupportedKeys...),
AbortMultipartUploadAction: condition.NewKeySet(condition.CommonKeys...), AbortMultipartUploadAction: condition.NewKeySet(condition.CommonKeys...),
@ -291,8 +313,6 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
DeleteBucketPolicyAction: condition.NewKeySet(condition.CommonKeys...), DeleteBucketPolicyAction: condition.NewKeySet(condition.CommonKeys...),
DeleteObjectAction: condition.NewKeySet(condition.CommonKeys...),
GetBucketLocationAction: condition.NewKeySet(condition.CommonKeys...), GetBucketLocationAction: condition.NewKeySet(condition.CommonKeys...),
GetBucketNotificationAction: condition.NewKeySet(condition.CommonKeys...), GetBucketNotificationAction: condition.NewKeySet(condition.CommonKeys...),
@ -303,6 +323,7 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
append([]condition.Key{ append([]condition.Key{
condition.S3XAmzServerSideEncryption, condition.S3XAmzServerSideEncryption,
condition.S3XAmzServerSideEncryptionCustomerAlgorithm, condition.S3XAmzServerSideEncryptionCustomerAlgorithm,
condition.S3VersionID,
}, condition.CommonKeys...)...), }, condition.CommonKeys...)...),
HeadBucketAction: condition.NewKeySet(condition.CommonKeys...), HeadBucketAction: condition.NewKeySet(condition.CommonKeys...),
@ -335,6 +356,11 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
PutBucketPolicyAction: condition.NewKeySet(condition.CommonKeys...), PutBucketPolicyAction: condition.NewKeySet(condition.CommonKeys...),
DeleteObjectAction: condition.NewKeySet(
append([]condition.Key{
condition.S3VersionID,
}, condition.CommonKeys...)...),
PutObjectAction: condition.NewKeySet( PutObjectAction: condition.NewKeySet(
append([]condition.Key{ append([]condition.Key{
condition.S3XAmzCopySource, condition.S3XAmzCopySource,
@ -342,6 +368,7 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
condition.S3XAmzServerSideEncryptionCustomerAlgorithm, condition.S3XAmzServerSideEncryptionCustomerAlgorithm,
condition.S3XAmzMetadataDirective, condition.S3XAmzMetadataDirective,
condition.S3XAmzStorageClass, condition.S3XAmzStorageClass,
condition.S3VersionID,
condition.S3ObjectLockRetainUntilDate, condition.S3ObjectLockRetainUntilDate,
condition.S3ObjectLockMode, condition.S3ObjectLockMode,
condition.S3ObjectLockLegalHold, condition.S3ObjectLockLegalHold,
@ -351,21 +378,32 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
// LockLegalHold is not supported with PutObjectRetentionAction // LockLegalHold is not supported with PutObjectRetentionAction
PutObjectRetentionAction: condition.NewKeySet( PutObjectRetentionAction: condition.NewKeySet(
append([]condition.Key{ append([]condition.Key{
condition.S3XAmzServerSideEncryption,
condition.S3XAmzServerSideEncryptionCustomerAlgorithm,
condition.S3ObjectLockRemainingRetentionDays, condition.S3ObjectLockRemainingRetentionDays,
condition.S3ObjectLockRetainUntilDate, condition.S3ObjectLockRetainUntilDate,
condition.S3ObjectLockMode, condition.S3ObjectLockMode,
condition.S3VersionID,
}, condition.CommonKeys...)...),
GetObjectRetentionAction: condition.NewKeySet(
append([]condition.Key{
condition.S3XAmzServerSideEncryption,
condition.S3XAmzServerSideEncryptionCustomerAlgorithm,
condition.S3VersionID,
}, condition.CommonKeys...)...), }, condition.CommonKeys...)...),
GetObjectRetentionAction: condition.NewKeySet(condition.CommonKeys...),
PutObjectLegalHoldAction: condition.NewKeySet( PutObjectLegalHoldAction: condition.NewKeySet(
append([]condition.Key{ append([]condition.Key{
condition.S3XAmzServerSideEncryption,
condition.S3XAmzServerSideEncryptionCustomerAlgorithm,
condition.S3ObjectLockLegalHold, condition.S3ObjectLockLegalHold,
condition.S3VersionID,
}, condition.CommonKeys...)...), }, condition.CommonKeys...)...),
GetObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...), GetObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...),
// https://docs.aws.amazon.com/AmazonS3/latest/dev/list_amazons3.html // https://docs.aws.amazon.com/AmazonS3/latest/dev/list_amazons3.html
BypassGovernanceRetentionAction: condition.NewKeySet( BypassGovernanceRetentionAction: condition.NewKeySet(
append([]condition.Key{ append([]condition.Key{
condition.S3VersionID,
condition.S3ObjectLockRemainingRetentionDays, condition.S3ObjectLockRemainingRetentionDays,
condition.S3ObjectLockRetainUntilDate, condition.S3ObjectLockRetainUntilDate,
condition.S3ObjectLockMode, condition.S3ObjectLockMode,
@ -376,11 +414,24 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...), PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
GetBucketTaggingAction: condition.NewKeySet(condition.CommonKeys...), GetBucketTaggingAction: condition.NewKeySet(condition.CommonKeys...),
PutBucketTaggingAction: condition.NewKeySet(condition.CommonKeys...), PutBucketTaggingAction: condition.NewKeySet(condition.CommonKeys...),
PutObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
GetObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
DeleteObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
PutObjectVersionTaggingAction: condition.NewKeySet(condition.CommonKeys...), PutObjectTaggingAction: condition.NewKeySet(
append([]condition.Key{
condition.S3VersionID,
}, condition.CommonKeys...)...),
GetObjectTaggingAction: condition.NewKeySet(
append([]condition.Key{
condition.S3VersionID,
}, condition.CommonKeys...)...),
DeleteObjectTaggingAction: condition.NewKeySet(
append([]condition.Key{
condition.S3VersionID,
}, condition.CommonKeys...)...),
PutObjectVersionTaggingAction: condition.NewKeySet(
append([]condition.Key{
condition.S3VersionID,
}, condition.CommonKeys...)...),
GetObjectVersionAction: condition.NewKeySet( GetObjectVersionAction: condition.NewKeySet(
append([]condition.Key{ append([]condition.Key{
condition.S3VersionID, condition.S3VersionID,
@ -397,10 +448,22 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
append([]condition.Key{ append([]condition.Key{
condition.S3VersionID, condition.S3VersionID,
}, condition.CommonKeys...)...), }, condition.CommonKeys...)...),
GetReplicationConfigurationAction: condition.NewKeySet(condition.CommonKeys...), GetReplicationConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
PutReplicationConfigurationAction: condition.NewKeySet(condition.CommonKeys...), PutReplicationConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
ReplicateObjectAction: condition.NewKeySet(condition.CommonKeys...), ReplicateObjectAction: condition.NewKeySet(
ReplicateDeleteAction: condition.NewKeySet(condition.CommonKeys...), append([]condition.Key{
ReplicateTagsAction: condition.NewKeySet(condition.CommonKeys...), condition.S3VersionID,
GetObjectVersionForReplicationAction: condition.NewKeySet(condition.CommonKeys...), }, condition.CommonKeys...)...),
ReplicateDeleteAction: condition.NewKeySet(
append([]condition.Key{
condition.S3VersionID,
}, condition.CommonKeys...)...),
ReplicateTagsAction: condition.NewKeySet(
append([]condition.Key{
condition.S3VersionID,
}, condition.CommonKeys...)...),
GetObjectVersionForReplicationAction: condition.NewKeySet(
append([]condition.Key{
condition.S3VersionID,
}, condition.CommonKeys...)...),
} }

View File

@ -43,6 +43,15 @@ func (actionSet ActionSet) Match(action Action) bool {
if r.Match(action) { if r.Match(action) {
return true return true
} }
// This is a special case where GetObjectVersion
// means GetObject is enabled implicitly.
switch r {
case GetObjectVersionAction:
if action == GetObjectAction {
return true
}
}
} }
return false return false

View File

@ -20,7 +20,9 @@ import (
"encoding/json" "encoding/json"
"net" "net"
"reflect" "reflect"
"strings"
"testing" "testing"
"time"
"github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio/pkg/bucket/policy" "github.com/minio/minio/pkg/bucket/policy"
@ -432,6 +434,259 @@ func TestPolicyIsValid(t *testing.T) {
} }
} }
// Parse config with location constraints
func TestPolicyParseConfig(t *testing.T) {
policy1LocationConstraint := `{
"Version":"2012-10-17",
"Statement":[
{
"Sid":"statement1",
"Effect":"Allow",
"Action": "s3:CreateBucket",
"Resource": "arn:aws:s3:::*",
"Condition": {
"StringLike": {
"s3:LocationConstraint": "us-east-1"
}
}
},
{
"Sid":"statement2",
"Effect":"Deny",
"Action": "s3:CreateBucket",
"Resource": "arn:aws:s3:::*",
"Condition": {
"StringNotLike": {
"s3:LocationConstraint": "us-east-1"
}
}
}
]
}`
policy2Condition := `{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "statement1",
"Effect": "Allow",
"Action": "s3:GetObjectVersion",
"Resource": "arn:aws:s3:::test/HappyFace.jpg"
},
{
"Sid": "statement2",
"Effect": "Deny",
"Action": "s3:GetObjectVersion",
"Resource": "arn:aws:s3:::test/HappyFace.jpg",
"Condition": {
"StringNotEquals": {
"s3:versionid": "AaaHbAQitwiL_h47_44lRO2DDfLlBO5e"
}
}
}
]
}`
policy3ConditionActionRegex := `{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "statement2",
"Effect": "Allow",
"Action": "s3:Get*",
"Resource": "arn:aws:s3:::test/HappyFace.jpg",
"Condition": {
"StringEquals": {
"s3:versionid": "AaaHbAQitwiL_h47_44lRO2DDfLlBO5e"
}
}
}
]
}`
policy4ConditionAction := `{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "statement2",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::test/HappyFace.jpg",
"Condition": {
"StringEquals": {
"s3:versionid": "AaaHbAQitwiL_h47_44lRO2DDfLlBO5e"
}
}
}
]
}`
policy5ConditionCurrenTime := `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:Get*",
"s3:Put*"
],
"Resource": [
"arn:aws:s3:::test/*"
],
"Condition": {
"DateGreaterThan": {
"aws:CurrentTime": [
"2017-02-28T00:00:00Z"
]
}
}
}
]
}`
policy5ConditionCurrenTimeLesser := `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:Get*",
"s3:Put*"
],
"Resource": [
"arn:aws:s3:::test/*"
],
"Condition": {
"DateLessThan": {
"aws:CurrentTime": [
"2017-02-28T00:00:00Z"
]
}
}
}
]
}`
tests := []struct {
p string
args Args
allowed bool
}{
{
p: policy1LocationConstraint,
allowed: true,
args: Args{
AccountName: "allowed",
Action: CreateBucketAction,
BucketName: "test",
ConditionValues: map[string][]string{"LocationConstraint": {"us-east-1"}},
},
},
{
p: policy1LocationConstraint,
allowed: false,
args: Args{
AccountName: "disallowed",
Action: CreateBucketAction,
BucketName: "test",
ConditionValues: map[string][]string{"LocationConstraint": {"us-east-2"}},
},
},
{
p: policy2Condition,
allowed: true,
args: Args{
AccountName: "allowed",
Action: GetObjectAction,
BucketName: "test",
ObjectName: "HappyFace.jpg",
ConditionValues: map[string][]string{"versionid": {"AaaHbAQitwiL_h47_44lRO2DDfLlBO5e"}},
},
},
{
p: policy2Condition,
allowed: false,
args: Args{
AccountName: "disallowed",
Action: GetObjectAction,
BucketName: "test",
ObjectName: "HappyFace.jpg",
ConditionValues: map[string][]string{"versionid": {"AaaHbAQitwiL_h47_44lRO2DDfLlBO5f"}},
},
},
{
p: policy3ConditionActionRegex,
allowed: true,
args: Args{
AccountName: "allowed",
Action: GetObjectAction,
BucketName: "test",
ObjectName: "HappyFace.jpg",
ConditionValues: map[string][]string{"versionid": {"AaaHbAQitwiL_h47_44lRO2DDfLlBO5e"}},
},
},
{
p: policy3ConditionActionRegex,
allowed: false,
args: Args{
AccountName: "disallowed",
Action: GetObjectAction,
BucketName: "test",
ObjectName: "HappyFace.jpg",
ConditionValues: map[string][]string{"versionid": {"AaaHbAQitwiL_h47_44lRO2DDfLlBO5f"}},
},
},
{
p: policy4ConditionAction,
allowed: true,
args: Args{
AccountName: "allowed",
Action: GetObjectAction,
BucketName: "test",
ObjectName: "HappyFace.jpg",
ConditionValues: map[string][]string{"versionid": {"AaaHbAQitwiL_h47_44lRO2DDfLlBO5e"}},
},
},
{
p: policy5ConditionCurrenTime,
allowed: true,
args: Args{
AccountName: "allowed",
Action: GetObjectAction,
BucketName: "test",
ObjectName: "HappyFace.jpg",
ConditionValues: map[string][]string{
"CurrentTime": {time.Now().Format(time.RFC3339)},
},
},
},
{
p: policy5ConditionCurrenTimeLesser,
allowed: false,
args: Args{
AccountName: "disallowed",
Action: GetObjectAction,
BucketName: "test",
ObjectName: "HappyFace.jpg",
ConditionValues: map[string][]string{
"CurrentTime": {time.Now().Format(time.RFC3339)},
},
},
},
}
for _, test := range tests {
test := test
t.Run(test.args.AccountName, func(t *testing.T) {
ip, err := ParseConfig(strings.NewReader(test.p))
if err != nil {
t.Error(err)
}
if got := ip.IsAllowed(test.args); got != test.allowed {
t.Errorf("Expected %t, got %t", test.allowed, got)
}
})
}
}
func TestPolicyUnmarshalJSONAndValidate(t *testing.T) { func TestPolicyUnmarshalJSONAndValidate(t *testing.T) {
case1Data := []byte(`{ case1Data := []byte(`{
"ID": "MyPolicyForMyBucket1", "ID": "MyPolicyForMyBucket1",

View File

@ -114,8 +114,13 @@ func (statement Statement) isValid() error {
return Errorf("unsupported Resource found %v for action %v", statement.Resources, action) return Errorf("unsupported Resource found %v for action %v", statement.Resources, action)
} }
condKeys, ok := iamActionConditionKeyMap.Lookup(action)
if !ok {
return Errorf("conditions are not supported for action %v", action)
}
keys := statement.Conditions.Keys() keys := statement.Conditions.Keys()
keyDiff := keys.Difference(actionConditionKeyMap[action]) keyDiff := keys.Difference(condKeys)
if !keyDiff.IsEmpty() { if !keyDiff.IsEmpty() {
return Errorf("unsupported condition keys '%v' used for action '%v'", keyDiff, action) return Errorf("unsupported condition keys '%v' used for action '%v'", keyDiff, action)
} }