policy-engine/iam/converter_test.go
Denis Kirillov a0a35bf4bf [#22] iam: Fix converters
Validate that actions and resources contain wildcard only at the end

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-11-28 17:56:36 +03:00

1314 lines
34 KiB
Go

package iam
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"testing"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
"github.com/stretchr/testify/require"
)
type mockUserResolver struct {
users map[string]string
buckets map[string]string
}
func newMockUserResolver(accountUsers []string, buckets []string) *mockUserResolver {
userMap := make(map[string]string, len(accountUsers))
for _, user := range accountUsers {
userMap[user] = user + "/resolvedValue"
}
bucketMap := make(map[string]string, len(buckets))
for _, bkt := range buckets {
bucketMap[bkt] = bkt + "/resolvedValues"
}
return &mockUserResolver{users: userMap, buckets: bucketMap}
}
func (m *mockUserResolver) GetUserAddress(account, user string) (string, error) {
key, ok := m.users[account+"/"+user]
if !ok {
return "", errors.New("not found")
}
return key, nil
}
func (m *mockUserResolver) GetUserKey(account, user string) (string, error) {
key, ok := m.users[account+"/"+user]
if !ok {
return "", errors.New("not found")
}
return key, nil
}
func (m *mockUserResolver) GetBucketCID(bkt string) (string, error) {
cnrID, ok := m.buckets[bkt]
if !ok {
return "", errors.New("not found")
}
return cnrID, nil
}
func TestConverters(t *testing.T) {
namespace := "root"
userName := "JohnDoe"
user := namespace + "/" + userName
principal := "arn:aws:iam::" + namespace + ":user/" + userName
bktName := "DOC-EXAMPLE-BUCKET"
objName := "object-name"
resource := bktName + "/*"
action := "PutObject"
mockResolver := newMockUserResolver([]string{user}, []string{bktName})
t.Run("valid policy", func(t *testing.T) {
p := Policy{
Version: "2012-10-17",
Statement: []Statement{{
Principal: map[PrincipalType][]string{
AWSPrincipalType: {principal},
},
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{"arn:aws:s3:::" + resource},
Conditions: map[string]Condition{
CondStringEquals: {
"s3:RequestObjectTag/Department": {"Finance"},
},
},
}},
}
expected := &chain.Chain{Rules: []chain.Rule{
{
Status: chain.Allow,
Actions: chain.Actions{Names: []string{action}},
Resources: chain.Resources{Names: []string{resource}},
Condition: []chain.Condition{
{
Op: chain.CondStringEquals,
Object: chain.ObjectRequest,
Key: s3.PropertyKeyOwner,
Value: mockResolver.users[user],
},
{
Op: chain.CondStringEquals,
Object: chain.ObjectRequest,
Key: "s3:RequestObjectTag/Department",
Value: "Finance",
},
},
},
}}
s3Chain, err := ConvertToS3Chain(p, mockResolver)
require.NoError(t, err)
require.Equal(t, expected, s3Chain)
})
t.Run("valid native policy", func(t *testing.T) {
p := Policy{
Version: "2012-10-17",
Statement: []Statement{{
Principal: map[PrincipalType][]string{
AWSPrincipalType: {principal},
},
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{"arn:aws:s3:::" + resource},
}},
}
expected := &chain.Chain{Rules: []chain.Rule{
{
Status: chain.Allow,
Actions: chain.Actions{Names: []string{action}},
Resources: chain.Resources{Names: []string{fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName])}},
Condition: []chain.Condition{
{
Op: chain.CondStringEquals,
Object: chain.ObjectRequest,
Key: native.PropertyKeyActorPublicKey,
Value: mockResolver.users[user],
},
},
},
}}
nativeChain, err := ConvertToNativeChain(p, mockResolver)
require.NoError(t, err)
require.Equal(t, expected, nativeChain)
})
t.Run("valid inverted policy", func(t *testing.T) {
p := Policy{
Version: "2012-10-17",
Statement: []Statement{{
NotPrincipal: map[PrincipalType][]string{
AWSPrincipalType: {principal},
},
Effect: DenyEffect,
NotAction: []string{"s3:PutObject"},
NotResource: []string{"arn:aws:s3:::" + resource},
}},
}
expected := &chain.Chain{Rules: []chain.Rule{
{
Status: chain.AccessDenied,
Actions: chain.Actions{Inverted: true, Names: []string{action}},
Resources: chain.Resources{Inverted: true, Names: []string{resource}},
Condition: []chain.Condition{
{
Op: chain.CondStringNotEquals,
Object: chain.ObjectRequest,
Key: s3.PropertyKeyOwner,
Value: mockResolver.users[user],
},
},
},
}}
s3Chain, err := ConvertToS3Chain(p, mockResolver)
require.NoError(t, err)
require.Equal(t, expected, s3Chain)
})
t.Run("valid policy map get action", func(t *testing.T) {
p := Policy{
Version: "2012-10-17",
Statement: []Statement{{
Principal: map[PrincipalType][]string{
AWSPrincipalType: {principal},
},
Effect: DenyEffect,
NotAction: []string{"s3:GetObject"},
NotResource: []string{"arn:aws:s3:::" + bktName + "/" + objName},
}},
}
expected := &chain.Chain{Rules: []chain.Rule{
{
Status: chain.AccessDenied,
Actions: chain.Actions{Inverted: true, Names: actionToOpMap["GetObject"]},
Resources: chain.Resources{Inverted: true, Names: []string{
fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName]),
}},
Condition: []chain.Condition{
{
Op: chain.CondStringEquals,
Object: chain.ObjectRequest,
Key: native.PropertyKeyActorPublicKey,
Value: mockResolver.users[user],
},
{
Op: chain.CondStringLike,
Object: chain.ObjectResource,
Key: PropertyKeyFilePath,
Value: objName,
},
},
},
}}
nativeChain, err := ConvertToNativeChain(p, mockResolver)
require.NoError(t, err)
require.Equal(t, expected, nativeChain)
})
t.Run("invalid policy (unsupported principal type)", func(t *testing.T) {
p := Policy{
Version: "2012-10-17",
Statement: []Statement{{
Principal: map[PrincipalType][]string{
"dummy": {principal},
},
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
}},
}
_, err := ConvertToS3Chain(p, mockResolver)
require.Error(t, err)
})
t.Run("invalid policy (missing resource)", func(t *testing.T) {
p := Policy{
Version: "2012-10-17",
Statement: []Statement{{
Principal: map[PrincipalType][]string{
AWSPrincipalType: {principal},
},
Effect: AllowEffect,
Action: []string{"s3:PutObject"},
}},
}
_, err := ConvertToS3Chain(p, mockResolver)
require.Error(t, err)
})
t.Run("invalid policy (not applicable native actions)", func(t *testing.T) {
p := Policy{
Version: "2012-10-17",
Statement: []Statement{{
Principal: map[PrincipalType][]string{
AWSPrincipalType: {principal},
},
Effect: AllowEffect,
Action: []string{"s3:AbortMultipartUpload"},
Resource: []string{"arn:aws:s3:::" + resource},
}},
}
_, err := ConvertToNativeChain(p, mockResolver)
require.Error(t, err)
})
}
func TestConvertToChainCondition(t *testing.T) {
principal := "arn:aws:iam::namespace:user/userName"
conditions := Conditions{
CondStringEquals: {"key1": {"val0", "val1"}},
CondStringNotEquals: {"key2": {"val2"}},
CondStringEqualsIgnoreCase: {"key3": {"val3"}},
CondStringNotEqualsIgnoreCase: {"key4": {"val4"}},
CondStringLike: {"key5": {"val5"}},
CondStringNotLike: {"key6": {"val6"}},
CondDateEquals: {"key7": {"2006-01-02T15:04:05+07:00"}},
CondDateNotEquals: {"key8": {"2006-01-02T15:04:05Z"}},
CondDateLessThan: {"key9": {"2006-01-02T15:04:05+06:00"}},
CondDateLessThanEquals: {"key10": {"2006-01-02T15:04:05+03:00"}},
CondDateGreaterThan: {"key11": {"2006-01-02T15:04:05-01:00"}},
CondDateGreaterThanEquals: {"key12": {"2006-01-02T15:04:05-03:00"}},
CondBool: {"key13": {"True"}},
CondIPAddress: {"key14": {"val14"}},
CondNotIPAddress: {"key15": {"val15"}},
CondArnEquals: {"key16": {"val16"}},
CondArnLike: {condKeyAWSPrincipalARN: {principal}},
CondArnNotEquals: {"key18": {"val18"}},
CondArnNotLike: {"key19": {"val19"}},
}
expectedCondition := []GroupedConditions{
{
Any: true,
Conditions: []chain.Condition{
{
Op: chain.CondStringEquals,
Object: chain.ObjectRequest,
Key: "key1",
Value: "val0",
},
{
Op: chain.CondStringEquals,
Object: chain.ObjectRequest,
Key: "key1",
Value: "val1",
},
},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringNotEquals,
Object: chain.ObjectRequest,
Key: "key2",
Value: "val2",
}},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringEqualsIgnoreCase,
Object: chain.ObjectRequest,
Key: "key3",
Value: "val3",
}},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringNotEqualsIgnoreCase,
Object: chain.ObjectRequest,
Key: "key4",
Value: "val4",
}},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringLike,
Object: chain.ObjectRequest,
Key: "key5",
Value: "val5",
}},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringNotLike,
Object: chain.ObjectRequest,
Key: "key6",
Value: "val6",
}},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringEquals,
Object: chain.ObjectRequest,
Key: "key7",
Value: "1136189045",
}},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringNotEquals,
Object: chain.ObjectRequest,
Key: "key8",
Value: "1136214245",
}},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringLessThan,
Object: chain.ObjectRequest,
Key: "key9",
Value: "1136192645",
}},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringLessThanEquals,
Object: chain.ObjectRequest,
Key: "key10",
Value: "1136203445",
}},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringGreaterThan,
Object: chain.ObjectRequest,
Key: "key11",
Value: "1136217845",
}},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringGreaterThanEquals,
Object: chain.ObjectRequest,
Key: "key12",
Value: "1136225045",
}},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringEqualsIgnoreCase,
Object: chain.ObjectRequest,
Key: "key13",
Value: "True",
}},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringLike,
Object: chain.ObjectRequest,
Key: "key14",
Value: "val14",
}},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringNotLike,
Object: chain.ObjectRequest,
Key: "key15",
Value: "val15",
}},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringEquals,
Object: chain.ObjectRequest,
Key: "key16",
Value: "val16",
}},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringLike,
Object: chain.ObjectRequest,
Key: condKeyAWSPrincipalARN,
Value: principal,
}},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringNotEquals,
Object: chain.ObjectRequest,
Key: "key18",
Value: "val18",
}},
},
{
Conditions: []chain.Condition{{
Op: chain.CondStringNotLike,
Object: chain.ObjectRequest,
Key: "key19",
Value: "val19",
}},
},
}
actualCondition, err := convertToChainCondition(conditions)
require.NoError(t, err)
require.ElementsMatch(t, expectedCondition, actualCondition)
}
func TestParsePrincipalARN(t *testing.T) {
for i, tc := range []struct {
principal string
account string
user string
error bool
}{
{
principal: "arn:aws:iam::root:user/user",
account: "root",
user: "user",
error: false,
},
{
principal: "arn:aws:iam::root:user/path/user/user2",
account: "root",
user: "user2",
error: false,
},
{
principal: "arn:aws:iam::root:user/",
error: true,
},
{
principal: "root:user/name",
error: true,
},
{
principal: "arn:aws:iam::root:user",
error: true,
},
{
principal: "arn:aws:iam::root:name",
error: true,
},
{
principal: "arn:aws:iam::root:user/path/user/",
error: true,
},
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
account, user, err := parsePrincipalAsIAMUser(tc.principal)
if tc.error {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.account, account)
require.Equal(t, tc.user, user)
})
}
}
func TestComplexNativeConditions(t *testing.T) {
namespace := "root"
userName1, userName2 := "user1", "user2"
user1, user2 := namespace+"/"+userName1, namespace+"/"+userName2
principal1 := "arn:aws:iam::" + namespace + ":user/" + userName1
principal2 := "arn:aws:iam::" + namespace + ":user/" + userName2
bktName1, bktName2, bktName3 := "bktName", "bktName2", "bktName3"
objName1 := "objName1"
resource1 := bktName1 + "/" + objName1
resource2 := bktName2 + "/*"
resource3 := bktName3 + "/*"
action := "PutObject"
key1, key2 := "key1", "key2"
val0, val1, val2 := "val0", "val1", "val2"
mockResolver := newMockUserResolver([]string{user1, user2}, []string{bktName1, bktName2, bktName3})
nativeResource1 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName1])
nativeResource2 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName2])
nativeResource3 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.buckets[bktName3])
p := Policy{
Version: "2012-10-17",
Statement: []Statement{{
Principal: map[PrincipalType][]string{
AWSPrincipalType: {principal1, principal2},
},
Effect: DenyEffect,
Action: []string{"s3:" + action},
Resource: []string{"arn:aws:s3:::" + resource1, "arn:aws:s3:::" + resource2, "arn:aws:s3:::" + resource3},
Conditions: map[string]Condition{
CondStringEquals: {key1: {val0, val1}},
CondStringLike: {key2: {val2}},
},
}},
}
expectedStatus := chain.AccessDenied
expectedActions := chain.Actions{Names: actionToOpMap[action]}
expectedResource1 := chain.Resources{Names: []string{nativeResource1}}
expectedResource23 := chain.Resources{Names: []string{nativeResource2, nativeResource3}}
user1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: native.PropertyKeyActorPublicKey, Value: mockResolver.users[user1]}
user2Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: native.PropertyKeyActorPublicKey, Value: mockResolver.users[user2]}
objectName1Condition := chain.Condition{Op: chain.CondStringLike, Object: chain.ObjectResource, Key: PropertyKeyFilePath, Value: objName1}
key1val0Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: key1, Value: val0}
key1val1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: key1, Value: val1}
key2val2Condition := chain.Condition{Op: chain.CondStringLike, Object: chain.ObjectRequest, Key: key2, Value: val2}
expected := &chain.Chain{Rules: []chain.Rule{
{
Status: expectedStatus,
Actions: expectedActions,
Resources: expectedResource1,
Condition: []chain.Condition{
user1Condition,
objectName1Condition,
key1val0Condition,
key2val2Condition,
},
},
{
Status: expectedStatus,
Actions: expectedActions,
Resources: expectedResource1,
Condition: []chain.Condition{
user1Condition,
objectName1Condition,
key1val1Condition,
key2val2Condition,
},
},
{
Status: expectedStatus,
Actions: expectedActions,
Resources: expectedResource1,
Condition: []chain.Condition{
user2Condition,
objectName1Condition,
key1val0Condition,
key2val2Condition,
},
},
{
Status: expectedStatus,
Actions: expectedActions,
Resources: expectedResource1,
Condition: []chain.Condition{
user2Condition,
objectName1Condition,
key1val1Condition,
key2val2Condition,
},
},
{
Status: expectedStatus,
Actions: expectedActions,
Resources: expectedResource23,
Condition: []chain.Condition{
user1Condition,
key1val0Condition,
key2val2Condition,
},
},
{
Status: expectedStatus,
Actions: expectedActions,
Resources: expectedResource23,
Condition: []chain.Condition{
user1Condition,
key1val1Condition,
key2val2Condition,
},
},
{
Status: expectedStatus,
Actions: expectedActions,
Resources: expectedResource23,
Condition: []chain.Condition{
user2Condition,
key1val0Condition,
key2val2Condition,
},
},
{
Status: expectedStatus,
Actions: expectedActions,
Resources: expectedResource23,
Condition: []chain.Condition{
user2Condition,
key1val1Condition,
key2val2Condition,
},
},
}}
nativeChain, err := ConvertToNativeChain(p, mockResolver)
require.NoError(t, err)
requireChainRulesMatch(t, expected.Rules, nativeChain.Rules)
s := inmemory.NewInMemory()
err = s.MorphRuleChainStorage().AddMorphRuleChain("name", engine.NamespaceTarget("ns"), nativeChain)
require.NoError(t, err)
for _, tc := range []struct {
name string
action string
resource string
resourceMap map[string]string
requestMap map[string]string
status chain.Status
}{
{
name: "bucket resource1, all conditions matched",
action: action,
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName2], "some-oid"),
resourceMap: map[string]string{
PropertyKeyFilePath: "any-object-name",
},
requestMap: map[string]string{
native.PropertyKeyActorPublicKey: mockResolver.users[user1],
key1: val0,
key2: val2,
},
status: chain.AccessDenied,
},
{
name: "bucket resource3, all conditions matched",
action: action,
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName3], "some-oid"),
resourceMap: map[string]string{
PropertyKeyFilePath: "any-object-name",
},
requestMap: map[string]string{
native.PropertyKeyActorPublicKey: mockResolver.users[user1],
key1: val0,
key2: val2,
},
status: chain.AccessDenied,
},
{
name: "bucket resource, user condition mismatched",
action: action,
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName2], "some-oid"),
resourceMap: map[string]string{
PropertyKeyFilePath: "any-object-name",
},
requestMap: map[string]string{
key1: val0,
key2: val2,
},
status: chain.NoRuleFound,
},
{
name: "bucket resource, key2 condition mismatched",
action: action,
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName3], "some-oid"),
resourceMap: map[string]string{
PropertyKeyFilePath: "any-object-name",
},
requestMap: map[string]string{
native.PropertyKeyActorPublicKey: mockResolver.users[user1],
key1: val0,
key2: val0,
},
status: chain.NoRuleFound,
},
{
name: "bucket resource, key1 condition mismatched",
action: action,
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName2], "some-oid"),
resourceMap: map[string]string{
PropertyKeyFilePath: "any-object-name",
},
requestMap: map[string]string{
native.PropertyKeyActorPublicKey: mockResolver.users[user1],
key2: val2,
},
status: chain.NoRuleFound,
},
{
name: "bucket/object resource, all conditions matched",
action: action,
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"),
resourceMap: map[string]string{
PropertyKeyFilePath: objName1,
},
requestMap: map[string]string{
native.PropertyKeyActorPublicKey: mockResolver.users[user1],
key1: val0,
key2: val2,
},
status: chain.AccessDenied,
},
{
name: "bucket/object resource, user condition mismatched",
action: action,
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"),
resourceMap: map[string]string{
PropertyKeyFilePath: objName1,
},
requestMap: map[string]string{
native.PropertyKeyActorPublicKey: "dummy",
key1: val0,
key2: val2,
},
status: chain.NoRuleFound,
},
{
name: "bucket/object resource, key1 condition mismatched",
action: action,
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"),
resourceMap: map[string]string{
PropertyKeyFilePath: objName1,
},
requestMap: map[string]string{
native.PropertyKeyActorPublicKey: "dummy",
key2: val2,
},
status: chain.NoRuleFound,
},
{
name: "bucket/object resource, key2 condition mismatched",
action: action,
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"),
resourceMap: map[string]string{
PropertyKeyFilePath: objName1,
},
requestMap: map[string]string{
native.PropertyKeyActorPublicKey: "dummy",
key1: val0,
key2: val0,
},
status: chain.NoRuleFound,
},
{
name: "bucket/object resource, object filepath condition mismatched",
action: action,
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.buckets[bktName1], "some-oid"),
resourceMap: map[string]string{
PropertyKeyFilePath: "any-object-name",
},
requestMap: map[string]string{
native.PropertyKeyActorPublicKey: mockResolver.users[user1],
key1: val0,
key2: val2,
},
status: chain.NoRuleFound,
},
{
name: "resource mismatched",
action: action,
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, "some-cid", "some-oid"),
resourceMap: map[string]string{
PropertyKeyFilePath: objName1,
},
requestMap: map[string]string{
native.PropertyKeyActorPublicKey: mockResolver.users[user1],
key1: val0,
key2: val2,
},
status: chain.NoRuleFound,
},
} {
t.Run(tc.name, func(t *testing.T) {
req := testutil.NewRequest(tc.action, testutil.NewResource(tc.resource, tc.resourceMap), tc.requestMap)
status, _, err := s.IsAllowed("name", "ns", req)
require.NoError(t, err)
require.Equal(t, tc.status.String(), status.String())
})
}
}
func TestComplexS3Conditions(t *testing.T) {
namespace := "root"
userName1, userName2 := "user1", "user2"
user1, user2 := namespace+"/"+userName1, namespace+"/"+userName2
principal1 := "arn:aws:iam::" + namespace + ":user/" + userName1
principal2 := "arn:aws:iam::" + namespace + ":user/" + userName2
bktName1, bktName2, bktName3 := "bktName", "bktName2", "bktName3"
objName1 := "objName1"
resource1 := bktName1 + "/" + objName1
resource2 := bktName2 + "/*"
resource3 := bktName3 + "/*"
action := "PutObject"
key1, key2 := "key1", "key2"
val0, val1, val2 := "val0", "val1", "val2"
mockResolver := newMockUserResolver([]string{user1, user2}, []string{bktName1, bktName2, bktName3})
p := Policy{
Version: "2012-10-17",
Statement: []Statement{{
Principal: map[PrincipalType][]string{
AWSPrincipalType: {principal1, principal2},
},
Effect: DenyEffect,
Action: []string{"s3:" + action},
Resource: []string{"arn:aws:s3:::" + resource1, "arn:aws:s3:::" + resource2, "arn:aws:s3:::" + resource3},
Conditions: map[string]Condition{
CondStringEquals: {key1: {val0, val1}},
CondStringLike: {key2: {val2}},
},
}},
}
expectedStatus := chain.AccessDenied
expectedActions := chain.Actions{Names: actionToOpMap[action]}
expectedResources := chain.Resources{Names: []string{resource1, resource2, resource3}}
user1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: s3.PropertyKeyOwner, Value: mockResolver.users[user1]}
user2Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: s3.PropertyKeyOwner, Value: mockResolver.users[user2]}
key1val0Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: key1, Value: val0}
key1val1Condition := chain.Condition{Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: key1, Value: val1}
key2val2Condition := chain.Condition{Op: chain.CondStringLike, Object: chain.ObjectRequest, Key: key2, Value: val2}
expected := &chain.Chain{Rules: []chain.Rule{
{
Status: expectedStatus,
Actions: expectedActions,
Resources: expectedResources,
Condition: []chain.Condition{
user1Condition,
key1val0Condition,
key2val2Condition,
},
},
{
Status: expectedStatus,
Actions: expectedActions,
Resources: expectedResources,
Condition: []chain.Condition{
user1Condition,
key1val1Condition,
key2val2Condition,
},
},
{
Status: expectedStatus,
Actions: expectedActions,
Resources: expectedResources,
Condition: []chain.Condition{
user2Condition,
key1val0Condition,
key2val2Condition,
},
},
{
Status: expectedStatus,
Actions: expectedActions,
Resources: expectedResources,
Condition: []chain.Condition{
user2Condition,
key1val1Condition,
key2val2Condition,
},
},
}}
s3Chain, err := ConvertToS3Chain(p, mockResolver)
require.NoError(t, err)
requireChainRulesMatch(t, expected.Rules, s3Chain.Rules)
s := inmemory.NewInMemory()
err = s.MorphRuleChainStorage().AddMorphRuleChain("name", engine.NamespaceTarget("ns"), s3Chain)
require.NoError(t, err)
for _, tc := range []struct {
name string
action string
resource string
resourceMap map[string]string
requestMap map[string]string
status chain.Status
}{
{
name: "bucket resource1, all conditions matched",
action: action,
resource: resource1,
requestMap: map[string]string{
s3.PropertyKeyOwner: mockResolver.users[user1],
key1: val0,
key2: val2,
},
status: chain.AccessDenied,
},
{
name: "bucket resource3, all conditions matched",
action: action,
resource: bktName3 + "/some-obj",
requestMap: map[string]string{
s3.PropertyKeyOwner: mockResolver.users[user1],
key1: val0,
key2: val2,
},
status: chain.AccessDenied,
},
{
name: "bucket resource, user condition mismatched",
action: action,
resource: bktName2 + "/some-obj",
requestMap: map[string]string{
key1: val0,
key2: val2,
},
status: chain.NoRuleFound,
},
{
name: "bucket resource, key2 condition mismatched",
action: action,
resource: bktName3 + "/some-obj",
requestMap: map[string]string{
s3.PropertyKeyOwner: mockResolver.users[user1],
key1: val0,
key2: val0,
},
status: chain.NoRuleFound,
},
{
name: "bucket resource, key1 condition mismatched",
action: action,
resource: bktName2 + "/some-obj",
requestMap: map[string]string{
s3.PropertyKeyOwner: mockResolver.users[user1],
key2: val2,
},
status: chain.NoRuleFound,
},
{
name: "bucket/object resource, all conditions matched",
action: action,
resource: resource1,
requestMap: map[string]string{
s3.PropertyKeyOwner: mockResolver.users[user1],
key1: val0,
key2: val2,
},
status: chain.AccessDenied,
},
{
name: "bucket/object resource, resource mismatched",
action: action,
resource: bktName1 + "/some-obj",
requestMap: map[string]string{
s3.PropertyKeyOwner: mockResolver.users[user1],
key1: val0,
key2: val2,
},
status: chain.NoRuleFound,
},
{
name: "bucket/object resource, user condition mismatched",
action: action,
resource: resource1,
requestMap: map[string]string{
s3.PropertyKeyOwner: "dummy",
key1: val0,
key2: val2,
},
status: chain.NoRuleFound,
},
{
name: "bucket/object resource, key1 condition mismatched",
action: action,
resource: resource1,
requestMap: map[string]string{
s3.PropertyKeyOwner: "dummy",
key2: val2,
},
status: chain.NoRuleFound,
},
{
name: "bucket/object resource, key2 condition mismatched",
action: action,
resource: resource1,
requestMap: map[string]string{
s3.PropertyKeyOwner: "dummy",
key1: val0,
key2: val0,
},
status: chain.NoRuleFound,
},
{
name: "resource mismatched",
action: action,
resource: "some-bkt/some-obj",
requestMap: map[string]string{
s3.PropertyKeyOwner: mockResolver.users[user1],
key1: val0,
key2: val2,
},
status: chain.NoRuleFound,
},
} {
t.Run(tc.name, func(t *testing.T) {
req := testutil.NewRequest(tc.action, testutil.NewResource(tc.resource, tc.resourceMap), tc.requestMap)
status, _, err := s.IsAllowed("name", "ns", req)
require.NoError(t, err)
require.Equal(t, tc.status.String(), status.String())
})
}
}
func TestWildcardConverters(t *testing.T) {
policy := `{"Version":"2012-10-17","Statement":{"Effect":"Allow", "Principal": "*", "Action":"*","Resource":"*"}}`
var p Policy
err := json.Unmarshal([]byte(policy), &p)
require.NoError(t, err)
_, err = ConvertToS3Chain(p, newMockUserResolver(nil, nil))
require.NoError(t, err)
_, err = ConvertToNativeChain(p, newMockUserResolver(nil, nil))
require.NoError(t, err)
}
func TestActionParsing(t *testing.T) {
for _, tc := range []struct {
action string
expected string
err bool
}{
{
action: "withoutPrefix",
expected: "",
err: true,
},
{
action: "s3:*Object",
expected: "",
err: true,
},
{
action: "*",
expected: "*",
},
{
action: "s3:PutObject",
expected: "PutObject",
},
{
action: "s3:Put*",
expected: "Put*",
},
{
action: "s3:*",
expected: "*",
},
{
action: "s3:",
expected: "",
},
} {
t.Run("", func(t *testing.T) {
actual, err := parseActionAsS3Action(tc.action)
if tc.err {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.expected, actual)
})
}
}
func TestPrincipalParsing(t *testing.T) {
for _, tc := range []struct {
principal string
expectedAccount string
expectedUser string
err bool
}{
{
principal: "withoutPrefix",
err: true,
},
{
principal: "*",
err: true,
},
{
principal: "arn:aws:iam:::dummy",
err: true,
},
{
principal: "arn:aws:iam::",
err: true,
},
{
principal: "arn:aws:iam:::dummy/test",
err: true,
},
{
principal: "arn:aws:iam:::user/",
err: true,
},
{
principal: "arn:aws:iam:::user/user/",
err: true,
},
{
principal: "arn:aws:iam:::user/name",
expectedUser: "name",
},
{
principal: "arn:aws:iam:::user/path/name",
expectedUser: "name",
},
{
principal: "arn:aws:iam::root:user/path/name",
expectedAccount: "root",
expectedUser: "name",
},
} {
t.Run("", func(t *testing.T) {
account, user, err := parsePrincipalAsIAMUser(tc.principal)
if tc.err {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.expectedAccount, account)
require.Equal(t, tc.expectedUser, user)
})
}
}
func TestResourceParsing(t *testing.T) {
for _, tc := range []struct {
resource string
expectedBucket string
expectedObject string
err bool
}{
{
resource: "withoutPrefixAnd",
err: true,
},
{
resource: "arn:aws:s3:::*/obj",
err: true,
},
{
resource: "arn:aws:s3:::bkt/*",
expectedBucket: "bkt",
expectedObject: "*",
},
{
resource: "arn:aws:s3:::bkt",
expectedBucket: "bkt",
expectedObject: "*",
},
{
resource: "arn:aws:s3:::bkt/",
expectedBucket: "bkt",
expectedObject: "*",
},
{
resource: "arn:aws:s3:::*",
expectedBucket: "*",
expectedObject: "*",
},
{
resource: "*",
expectedBucket: "*",
expectedObject: "*",
},
} {
t.Run("", func(t *testing.T) {
bkt, obj, err := parseResourceAsS3ARN(tc.resource)
if tc.err {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.expectedBucket, bkt)
require.Equal(t, tc.expectedObject, obj)
})
}
}
func requireChainRulesMatch(t *testing.T, expected, actual []chain.Rule) {
require.Equal(t, len(expected), len(actual), "length of chain rules differ")
seen := make(map[int]int)
for i, expRule := range expected {
for j, actRule := range actual {
if _, ok := seen[j]; ok {
continue
}
if areRulesMatched(expRule, actRule) {
seen[j] = i
break
}
}
}
require.Len(t, seen, len(expected), "expected unique rules")
}
func areRulesMatched(rule1, rule2 chain.Rule) bool {
if rule1.Status != rule2.Status ||
rule1.Any != rule2.Any ||
rule1.Resources.Inverted != rule2.Resources.Inverted ||
len(rule1.Resources.Names) != len(rule2.Resources.Names) ||
rule1.Actions.Inverted != rule2.Actions.Inverted ||
len(rule1.Actions.Names) != len(rule2.Actions.Names) {
return false
}
for i, name := range rule1.Resources.Names {
if name != rule2.Resources.Names[i] {
return false
}
}
for i, name := range rule1.Actions.Names {
if name != rule2.Actions.Names[i] {
return false
}
}
seen := make(map[int]struct{})
for _, cond1 := range rule1.Condition {
for j, cond2 := range rule2.Condition {
if _, ok := seen[j]; ok {
continue
}
if cond1 == cond2 {
seen[j] = struct{}{}
break
}
}
}
return len(seen) == len(rule1.Condition)
}