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 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}}, 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}}, 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.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"}}, } 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.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:Request/scope": "*" } } } ] } ` expectedConditions := []chain.Condition{ { Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: "frostfsid:userClaim/tag-department", Value: "hr", }, { Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: "aws:ResourceTag/owner", Value: "hr-admin", }, { Op: chain.CondStringEquals, Object: chain.ObjectRequest, Key: "aws:Request/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) } }