forked from TrueCloudLab/policy-engine
Denis Kirillov
1f190e1668
We have to add native:PutObject when want to delete object because of tombstone must be created (it's a put operation) Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
1725 lines
48 KiB
Go
1725 lines
48 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/common"
|
|
"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
|
|
containers map[string]string
|
|
namespace string
|
|
}
|
|
|
|
func newMockUserResolver(accountUsers []string, buckets []string, namespace string) *mockUserResolver {
|
|
userMap := make(map[string]string, len(accountUsers))
|
|
for _, user := range accountUsers {
|
|
userMap[user] = user + "/resolvedValue"
|
|
}
|
|
|
|
containerMap := make(map[string]string, len(buckets))
|
|
for _, bkt := range buckets {
|
|
containerMap[bkt] = bkt + "/resolvedValues"
|
|
}
|
|
|
|
return &mockUserResolver{users: userMap, containers: containerMap, namespace: namespace}
|
|
}
|
|
|
|
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) GetBucketInfo(bkt string) (*BucketInfo, error) {
|
|
cnr, ok := m.containers[bkt]
|
|
if !ok {
|
|
return nil, errors.New("not found")
|
|
}
|
|
|
|
return &BucketInfo{Container: cnr, Namespace: m.namespace}, 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 := fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName)
|
|
s3GetObjectAction := "s3:GetObject"
|
|
s3HeadObjectAction := "s3:HeadObject"
|
|
|
|
mockResolver := newMockUserResolver([]string{user}, []string{bktName}, namespace)
|
|
|
|
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{s3GetObjectAction},
|
|
Resource: []string{resource},
|
|
Conditions: map[string]Condition{
|
|
CondStringEquals: {
|
|
"s3:RequestObjectTag/Department": {"Finance"},
|
|
},
|
|
},
|
|
}},
|
|
}
|
|
|
|
expected := &chain.Chain{Rules: []chain.Rule{
|
|
{
|
|
Status: chain.Allow,
|
|
Actions: chain.Actions{Names: []string{s3GetObjectAction, s3HeadObjectAction}},
|
|
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)
|
|
assertChainsEqual(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{resource},
|
|
}},
|
|
}
|
|
|
|
expected := &chain.Chain{Rules: []chain.Rule{
|
|
{
|
|
Status: chain.Allow,
|
|
Actions: chain.Actions{Names: []string{native.MethodGetContainer, native.MethodPutObject}},
|
|
Resources: chain.Resources{Names: []string{
|
|
fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, namespace, mockResolver.containers[bktName]),
|
|
fmt.Sprintf(native.ResourceFormatNamespaceContainer, namespace, mockResolver.containers[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)
|
|
assertChainsEqual(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{s3GetObjectAction},
|
|
NotResource: []string{resource},
|
|
}},
|
|
}
|
|
|
|
expected := &chain.Chain{Rules: []chain.Rule{
|
|
{
|
|
Status: chain.AccessDenied,
|
|
Actions: chain.Actions{Inverted: true, Names: []string{s3GetObjectAction, s3HeadObjectAction}},
|
|
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)
|
|
assertChainsEqual(t, expected, s3Chain)
|
|
})
|
|
|
|
t.Run("valid native policy map action", func(t *testing.T) {
|
|
p := Policy{
|
|
Version: "2012-10-17",
|
|
Statement: []Statement{{
|
|
Principal: map[PrincipalType][]string{
|
|
AWSPrincipalType: {principal},
|
|
},
|
|
Effect: AllowEffect,
|
|
Action: []string{"s3:DeleteObject", "s3:DeleteBucket"},
|
|
Resource: []string{
|
|
fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName, objName),
|
|
fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName),
|
|
},
|
|
}},
|
|
}
|
|
|
|
expected := &chain.Chain{Rules: []chain.Rule{
|
|
{
|
|
Status: chain.Allow,
|
|
Actions: chain.Actions{Names: []string{native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject, native.MethodDeleteObject, native.MethodPutObject}},
|
|
Resources: chain.Resources{Names: []string{
|
|
fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, namespace, mockResolver.containers[bktName]),
|
|
fmt.Sprintf(native.ResourceFormatNamespaceContainer, namespace, mockResolver.containers[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,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Status: chain.Allow,
|
|
Actions: chain.Actions{Names: []string{native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject, native.MethodDeleteObject, native.MethodPutObject}},
|
|
Resources: chain.Resources{Names: []string{
|
|
fmt.Sprintf(native.ResourceFormatNamespaceContainer, namespace, mockResolver.containers[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)
|
|
assertChainsEqual(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)
|
|
})
|
|
|
|
t.Run("invalid policy (missing s3 actions)", func(t *testing.T) {
|
|
p := Policy{
|
|
Version: "2012-10-17",
|
|
Statement: []Statement{{
|
|
Principal: map[PrincipalType][]string{
|
|
AWSPrincipalType: {principal},
|
|
},
|
|
Effect: AllowEffect,
|
|
Resource: []string{"arn:aws:s3:::" + resource},
|
|
}},
|
|
}
|
|
|
|
_, err := ConvertToS3Chain(p, mockResolver)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("valid mixed iam/s3 actions", func(t *testing.T) {
|
|
p := Policy{
|
|
Version: "2012-10-17",
|
|
Statement: []Statement{{
|
|
Principal: map[PrincipalType][]string{AWSPrincipalType: {principal}},
|
|
Effect: AllowEffect,
|
|
Action: []string{"s3:DeleteObject", "iam:*"},
|
|
Resource: []string{"*"},
|
|
}},
|
|
}
|
|
|
|
s3Expected := &chain.Chain{Rules: []chain.Rule{{
|
|
Status: chain.Allow,
|
|
Actions: chain.Actions{Names: []string{"s3:DeleteObject", "s3:DeleteMultipleObjects", "iam:*"}},
|
|
Resources: chain.Resources{Names: []string{"*"}},
|
|
Condition: []chain.Condition{{
|
|
Op: chain.CondStringEquals,
|
|
Object: chain.ObjectRequest,
|
|
Key: s3.PropertyKeyOwner,
|
|
Value: mockResolver.users[user],
|
|
}},
|
|
}}}
|
|
|
|
s3Chain, err := ConvertToS3Chain(p, mockResolver)
|
|
require.NoError(t, err)
|
|
assertChainsEqual(t, s3Expected, s3Chain)
|
|
|
|
nativeExpected := &chain.Chain{Rules: []chain.Rule{{
|
|
Status: chain.Allow,
|
|
Actions: chain.Actions{Names: []string{native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject}},
|
|
Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects, native.ResourceFormatAllContainers}},
|
|
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)
|
|
assertChainsEqual(t, nativeExpected, nativeChain)
|
|
})
|
|
}
|
|
|
|
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"}},
|
|
CondNumericEquals: {"key20": {"-20"}},
|
|
CondNumericNotEquals: {"key21": {"+21"}},
|
|
CondNumericLessThan: {"key22": {"0"}},
|
|
CondNumericLessThanEquals: {"key23": {"23.23"}},
|
|
CondNumericGreaterThan: {"key24": {"-24.24"}},
|
|
CondNumericGreaterThanEquals: {"key25": {"+25.25"}},
|
|
}
|
|
|
|
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",
|
|
}},
|
|
},
|
|
{
|
|
Conditions: []chain.Condition{{
|
|
Op: chain.CondNumericEquals,
|
|
Object: chain.ObjectRequest,
|
|
Key: "key20",
|
|
Value: "-20",
|
|
}},
|
|
},
|
|
{
|
|
Conditions: []chain.Condition{{
|
|
Op: chain.CondNumericNotEquals,
|
|
Object: chain.ObjectRequest,
|
|
Key: "key21",
|
|
Value: "+21",
|
|
}},
|
|
},
|
|
{
|
|
Conditions: []chain.Condition{{
|
|
Op: chain.CondNumericLessThan,
|
|
Object: chain.ObjectRequest,
|
|
Key: "key22",
|
|
Value: "0",
|
|
}},
|
|
},
|
|
{
|
|
Conditions: []chain.Condition{{
|
|
Op: chain.CondNumericLessThanEquals,
|
|
Object: chain.ObjectRequest,
|
|
Key: "key23",
|
|
Value: "23.23",
|
|
}},
|
|
},
|
|
{
|
|
Conditions: []chain.Condition{{
|
|
Op: chain.CondNumericGreaterThan,
|
|
Object: chain.ObjectRequest,
|
|
Key: "key24",
|
|
Value: "-24.24",
|
|
}},
|
|
},
|
|
{
|
|
Conditions: []chain.Condition{{
|
|
Op: chain.CondNumericGreaterThanEquals,
|
|
Object: chain.ObjectRequest,
|
|
Key: "key25",
|
|
Value: "+25.25",
|
|
}},
|
|
},
|
|
}
|
|
|
|
actualCondition, err := convertToChainCondition(conditions)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, expectedCondition, actualCondition)
|
|
|
|
invalidConditions := []Condition{
|
|
{"key1": {"invalid"}},
|
|
{"key2": {"1 2"}},
|
|
{"key3": {"0x12f"}},
|
|
{"key4": {"0b1010"}},
|
|
{"key5": {"+Inf"}},
|
|
{"key6": {"-Inf"}},
|
|
{"key7": {"inf"}},
|
|
{"key8": {"NaN"}},
|
|
{"key9": {"nan"}},
|
|
}
|
|
|
|
for _, cond := range invalidConditions {
|
|
_, err = convertToChainCondition(Conditions{CondNumericEquals: cond})
|
|
require.Error(t, err)
|
|
}
|
|
}
|
|
|
|
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.containers[bktName1])
|
|
nativeResource1cnr := fmt.Sprintf(native.ResourceFormatRootContainer, mockResolver.containers[bktName1])
|
|
nativeResource2 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName2])
|
|
nativeResource2cnr := fmt.Sprintf(native.ResourceFormatRootContainer, mockResolver.containers[bktName2])
|
|
nativeResource3 := fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers[bktName3])
|
|
nativeResource3cnr := fmt.Sprintf(native.ResourceFormatRootContainer, mockResolver.containers[bktName3])
|
|
|
|
p := Policy{
|
|
Version: "2012-10-17",
|
|
Statement: []Statement{{
|
|
Principal: map[PrincipalType][]string{
|
|
AWSPrincipalType: {principal1, principal2},
|
|
},
|
|
Effect: AllowEffect,
|
|
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.Allow
|
|
expectedActions := chain.Actions{Names: actionToNativeOpMap["s3:"+action]}
|
|
expectedResource1 := chain.Resources{Names: []string{nativeResource1, nativeResource1cnr}}
|
|
expectedResource23 := chain.Resources{Names: []string{nativeResource2, nativeResource2cnr, nativeResource3, nativeResource3cnr}}
|
|
|
|
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.containers[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.Allow,
|
|
},
|
|
{
|
|
name: "bucket resource3, all conditions matched",
|
|
action: action,
|
|
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[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.Allow,
|
|
},
|
|
{
|
|
name: "bucket resource, user condition mismatched",
|
|
action: action,
|
|
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[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.containers[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.containers[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.containers[bktName1], "some-oid"),
|
|
resourceMap: map[string]string{
|
|
PropertyKeyFilePath: objName1,
|
|
},
|
|
requestMap: map[string]string{
|
|
native.PropertyKeyActorPublicKey: mockResolver.users[user1],
|
|
key1: val0,
|
|
key2: val2,
|
|
},
|
|
status: chain.Allow,
|
|
},
|
|
{
|
|
name: "bucket/object resource, user condition mismatched",
|
|
action: action,
|
|
resource: fmt.Sprintf(native.ResourceFormatRootContainerObject, mockResolver.containers[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.containers[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.containers[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.containers[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", engine.NewRequestTargetWithNamespace("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 := fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName1, objName1)
|
|
resource2 := fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName2)
|
|
resource3 := fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName3)
|
|
action := "s3:DeleteObject"
|
|
action2 := "s3:DeleteMultipleObjects"
|
|
|
|
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{action},
|
|
Resource: []string{resource1, resource2, resource3},
|
|
Conditions: map[string]Condition{
|
|
CondStringEquals: {key1: {val0, val1}},
|
|
CondStringLike: {key2: {val2}},
|
|
},
|
|
}},
|
|
}
|
|
|
|
expectedStatus := chain.AccessDenied
|
|
expectedActions := chain.Actions{Names: []string{action, action2}}
|
|
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: fmt.Sprintf(s3.ResourceFormatS3BucketObject, 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: fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName2, "some-obj"),
|
|
requestMap: map[string]string{
|
|
key1: val0,
|
|
key2: val2,
|
|
},
|
|
status: chain.NoRuleFound,
|
|
},
|
|
{
|
|
name: "bucket resource, key2 condition mismatched",
|
|
action: action,
|
|
resource: fmt.Sprintf(s3.ResourceFormatS3BucketObject, 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: fmt.Sprintf(s3.ResourceFormatS3BucketObject, 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: fmt.Sprintf(s3.ResourceFormatS3BucketObject, 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: fmt.Sprintf(s3.ResourceFormatS3BucketObject, "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", engine.NewRequestTargetWithNamespace("ns"), req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.status.String(), status.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestS3BucketResource(t *testing.T) {
|
|
namespace := "ns"
|
|
bktName1, bktName2 := "bucket1", "bucket2"
|
|
chainName := chain.Name("name")
|
|
|
|
mockResolver := newMockUserResolver([]string{}, []string{}, "")
|
|
|
|
p := Policy{
|
|
Version: "2012-10-17",
|
|
Statement: []Statement{
|
|
{
|
|
Principal: map[PrincipalType][]string{Wildcard: nil},
|
|
Effect: DenyEffect,
|
|
Action: []string{"s3:ListBucket"},
|
|
Resource: []string{fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName1)},
|
|
},
|
|
{
|
|
Principal: map[PrincipalType][]string{Wildcard: nil},
|
|
Effect: AllowEffect,
|
|
Action: []string{"*"},
|
|
Resource: []string{s3.ResourceFormatS3All},
|
|
},
|
|
},
|
|
}
|
|
|
|
s3Chain, err := ConvertToS3Chain(p, mockResolver)
|
|
require.NoError(t, err)
|
|
|
|
s := inmemory.NewInMemory()
|
|
_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chainName, engine.NamespaceTarget(namespace), s3Chain)
|
|
require.NoError(t, err)
|
|
|
|
// check we match just "bucket1" resource
|
|
req := testutil.NewRequest("s3:HeadBucket", testutil.NewResource(fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName1), nil), nil)
|
|
status, _, err := s.IsAllowed(chainName, engine.NewRequestTargetWithNamespace(namespace), req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, chain.AccessDenied.String(), status.String())
|
|
|
|
// check we match just "bucket2" resource
|
|
req = testutil.NewRequest("s3:HeadBucket", testutil.NewResource(fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName2), nil), nil)
|
|
status, _, err = s.IsAllowed(chainName, engine.NewRequestTargetWithNamespace(namespace), req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, chain.Allow.String(), status.String())
|
|
|
|
// check we also match "bucket2/object" resource
|
|
req = testutil.NewRequest("s3:PutObject", testutil.NewResource(fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName2, "object"), nil), nil)
|
|
status, _, err = s.IsAllowed(chainName, engine.NewRequestTargetWithNamespace(namespace), req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, chain.Allow.String(), status.String())
|
|
}
|
|
|
|
func TestWildcardConverters(t *testing.T) {
|
|
policy := `{"Version":"2012-10-17","Statement":{"Effect":"Allow", "Principal": "*", "Action":"s3:*","Resource":"*"}}`
|
|
|
|
var p Policy
|
|
err := json.Unmarshal([]byte(policy), &p)
|
|
require.NoError(t, err)
|
|
|
|
s3Expected := &chain.Chain{
|
|
Rules: []chain.Rule{{
|
|
Status: chain.Allow,
|
|
Actions: chain.Actions{Names: []string{"s3:*"}},
|
|
Resources: chain.Resources{Names: []string{Wildcard}},
|
|
}},
|
|
}
|
|
|
|
s3Chain, err := ConvertToS3Chain(p, newMockUserResolver(nil, nil, ""))
|
|
require.NoError(t, err)
|
|
require.Equal(t, s3Expected, s3Chain)
|
|
|
|
nativeExpected := &chain.Chain{
|
|
Rules: []chain.Rule{{
|
|
Status: chain.Allow,
|
|
Actions: chain.Actions{Names: []string{Wildcard}},
|
|
Resources: chain.Resources{Names: []string{native.ResourceFormatAllObjects, native.ResourceFormatAllContainers}},
|
|
}},
|
|
}
|
|
|
|
nativeChain, err := ConvertToNativeChain(p, newMockUserResolver(nil, nil, ""))
|
|
require.NoError(t, err)
|
|
require.Equal(t, nativeExpected, nativeChain)
|
|
}
|
|
|
|
func TestWildcardObjectsConverters(t *testing.T) {
|
|
policy := `{"Version":"2012-10-17","Statement":{"Effect":"Allow", "Principal": "*", "Action":"s3:*","Resource":"arn:aws:s3:::bucket/*"}}`
|
|
|
|
var p Policy
|
|
err := json.Unmarshal([]byte(policy), &p)
|
|
require.NoError(t, err)
|
|
|
|
s3Expected := &chain.Chain{
|
|
Rules: []chain.Rule{{
|
|
Status: chain.Allow,
|
|
Actions: chain.Actions{Names: []string{"s3:*"}},
|
|
Resources: chain.Resources{Names: []string{"arn:aws:s3:::bucket/*"}},
|
|
}},
|
|
}
|
|
|
|
s3Chain, err := ConvertToS3Chain(p, newMockUserResolver(nil, nil, ""))
|
|
require.NoError(t, err)
|
|
require.Equal(t, s3Expected, s3Chain)
|
|
|
|
mockResolver := newMockUserResolver(nil, []string{"bucket"}, "")
|
|
|
|
nativeExpected := &chain.Chain{
|
|
Rules: []chain.Rule{{
|
|
Status: chain.Allow,
|
|
Actions: chain.Actions{Names: []string{Wildcard}},
|
|
Resources: chain.Resources{Names: []string{
|
|
fmt.Sprintf(native.ResourceFormatRootContainer, mockResolver.containers["bucket"]),
|
|
fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers["bucket"]),
|
|
}},
|
|
}},
|
|
}
|
|
|
|
nativeChain, err := ConvertToNativeChain(p, mockResolver)
|
|
require.NoError(t, err)
|
|
assertChainsEqual(t, nativeExpected, nativeChain)
|
|
}
|
|
|
|
func TestDisableNativeDeny(t *testing.T) {
|
|
policy := `
|
|
{
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Effect": "Deny",
|
|
"Principal": "*",
|
|
"Action": "s3:*",
|
|
"Resource": [ "arn:aws:s3:::test-bucket/*" ]
|
|
}
|
|
]
|
|
}
|
|
`
|
|
var p Policy
|
|
err := json.Unmarshal([]byte(policy), &p)
|
|
require.NoError(t, err)
|
|
|
|
_, err = ConvertToNativeChain(p, newMockUserResolver(nil, nil, ""))
|
|
require.ErrorIs(t, err, ErrActionsNotApplicable)
|
|
}
|
|
|
|
func TestFromActions(t *testing.T) {
|
|
t.Run("s3 actions", func(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
action string
|
|
res []string
|
|
err bool
|
|
}{
|
|
{
|
|
action: "withoutPrefix",
|
|
err: true,
|
|
},
|
|
{
|
|
action: "s3:*Object",
|
|
err: true,
|
|
},
|
|
{
|
|
action: "*",
|
|
res: []string{Wildcard},
|
|
},
|
|
{
|
|
action: "s3:PutObject",
|
|
res: []string{"s3:PutObject", "s3:PostObject", "s3:CopyObject",
|
|
"s3:UploadPart", "s3:UploadPartCopy", "s3:CreateMultipartUpload", "s3:CompleteMultipartUpload"},
|
|
},
|
|
{
|
|
action: "s3:Put*",
|
|
err: true,
|
|
},
|
|
{
|
|
action: "s3:*",
|
|
res: []string{"s3:*"},
|
|
},
|
|
{
|
|
action: "s3:",
|
|
err: true,
|
|
},
|
|
{
|
|
action: "iam:ListAccessKeys",
|
|
res: []string{"iam:ListAccessKeys"},
|
|
},
|
|
{
|
|
action: "iam:*",
|
|
res: []string{"iam:*"},
|
|
},
|
|
} {
|
|
t.Run("", func(t *testing.T) {
|
|
actions, err := formS3ActionNames([]string{tc.action})
|
|
if tc.err {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, tc.res, actions)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("native actions", func(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
action string
|
|
res []string
|
|
err bool
|
|
}{
|
|
{
|
|
action: "withoutPrefix",
|
|
err: true,
|
|
},
|
|
{
|
|
action: "s3:*Object",
|
|
err: true,
|
|
},
|
|
{
|
|
action: "*",
|
|
res: []string{Wildcard},
|
|
},
|
|
{
|
|
action: "s3:PutObject",
|
|
res: []string{native.MethodGetContainer, native.MethodPutObject},
|
|
},
|
|
{
|
|
action: "s3:Put*",
|
|
err: true,
|
|
},
|
|
{
|
|
action: "s3:*",
|
|
res: []string{Wildcard},
|
|
},
|
|
{
|
|
action: "s3:",
|
|
err: true,
|
|
},
|
|
{
|
|
action: "iam:ListAccessKeys",
|
|
res: []string{},
|
|
},
|
|
{
|
|
action: "iam:*",
|
|
res: []string{},
|
|
},
|
|
} {
|
|
t.Run("", func(t *testing.T) {
|
|
actions, err := formNativeActionNames([]string{tc.action})
|
|
if tc.err {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, tc.res, actions)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
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
|
|
err bool
|
|
}{
|
|
{resource: "withoutPrefixAnd", err: true},
|
|
{resource: "arn:aws:s3:::*/obj", err: true},
|
|
{resource: "arn:aws:s3:::bkt/*"},
|
|
{resource: "arn:aws:s3:::bkt"},
|
|
{resource: "arn:aws:s3:::bkt/"},
|
|
{resource: "arn:aws:s3:::*"},
|
|
{resource: "*"},
|
|
} {
|
|
t.Run("", func(t *testing.T) {
|
|
err := validateResource(tc.resource)
|
|
if tc.err {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTagsConditions(t *testing.T) {
|
|
policy := `
|
|
{
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Effect": "Allow",
|
|
"Principal": "*",
|
|
"Action": "s3:PutObjectTagging",
|
|
"Resource": "*",
|
|
"Condition": {
|
|
"StringEquals": {
|
|
"aws:PrincipalTag/department": "hr",
|
|
"aws:ResourceTag/owner": "hr-admin",
|
|
"aws:RequestTag/scope": "*"
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
expectedConditions := []chain.Condition{
|
|
{
|
|
Op: chain.CondStringEquals,
|
|
Object: chain.ObjectRequest,
|
|
Key: fmt.Sprintf(common.PropertyKeyFormatFrostFSIDUserClaim, "tag-department"),
|
|
Value: "hr",
|
|
},
|
|
{
|
|
Op: chain.CondStringEquals,
|
|
Object: chain.ObjectRequest,
|
|
Key: fmt.Sprintf(s3.PropertyKeyFormatResourceTag, "owner"),
|
|
Value: "hr-admin",
|
|
},
|
|
{
|
|
Op: chain.CondStringEquals,
|
|
Object: chain.ObjectRequest,
|
|
Key: fmt.Sprintf(s3.PropertyKeyFormatRequestTag, "scope"),
|
|
Value: "*",
|
|
},
|
|
}
|
|
|
|
var p Policy
|
|
err := json.Unmarshal([]byte(policy), &p)
|
|
require.NoError(t, err)
|
|
|
|
s3Chain, err := ConvertToS3Chain(p, newMockUserResolver(nil, nil, ""))
|
|
require.NoError(t, err)
|
|
require.Len(t, s3Chain.Rules, 1)
|
|
require.ElementsMatch(t, expectedConditions, s3Chain.Rules[0].Condition)
|
|
|
|
nativeChain, err := ConvertToNativeChain(p, newMockUserResolver(nil, nil, ""))
|
|
require.NoError(t, err)
|
|
require.Len(t, nativeChain.Rules, 1)
|
|
require.ElementsMatch(t, expectedConditions, nativeChain.Rules[0].Condition)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
seen := make(map[int]struct{})
|
|
for _, name1 := range rule1.Resources.Names {
|
|
for j, name2 := range rule2.Resources.Names {
|
|
if _, ok := seen[j]; ok {
|
|
continue
|
|
}
|
|
if name1 == name2 {
|
|
seen[j] = struct{}{}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if len(seen) != len(rule1.Resources.Names) {
|
|
return false
|
|
}
|
|
|
|
seen = make(map[int]struct{})
|
|
for _, name1 := range rule1.Actions.Names {
|
|
for j, name2 := range rule2.Actions.Names {
|
|
if _, ok := seen[j]; ok {
|
|
continue
|
|
}
|
|
if name1 == name2 {
|
|
seen[j] = struct{}{}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if len(seen) != len(rule1.Actions.Names) {
|
|
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)
|
|
}
|
|
|
|
func assertChainsEqual(t *testing.T, chain1, chain2 *chain.Chain) {
|
|
require.Equal(t, string(chain1.ID), string(chain2.ID))
|
|
require.Equal(t, chain1.MatchType, chain2.MatchType)
|
|
require.Equal(t, len(chain1.Rules), len(chain2.Rules))
|
|
|
|
for i, rule := range chain1.Rules {
|
|
require.Equal(t, rule.Any, chain2.Rules[i].Any)
|
|
require.Equal(t, rule.Resources.Inverted, chain2.Rules[i].Resources.Inverted)
|
|
require.ElementsMatch(t, rule.Resources.Names, chain2.Rules[i].Resources.Names)
|
|
require.Equal(t, rule.Status, chain2.Rules[i].Status)
|
|
require.ElementsMatch(t, rule.Condition, chain2.Rules[i].Condition)
|
|
require.Equal(t, rule.Actions.Inverted, chain2.Rules[i].Actions.Inverted)
|
|
require.ElementsMatch(t, rule.Actions.Names, chain2.Rules[i].Actions.Names)
|
|
}
|
|
}
|