package iam import ( "errors" "strconv" "testing" policyengine "git.frostfs.info/TrueCloudLab/policy-engine" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/stretchr/testify/require" ) type mockUserResolver struct { users map[string]*keys.PublicKey } func newMockUserResolver(t *testing.T, accountUsers []string) *mockUserResolver { m := make(map[string]*keys.PublicKey, len(accountUsers)) for _, user := range accountUsers { key, err := keys.NewPrivateKey() require.NoError(t, err) m[user] = key.PublicKey() } return &mockUserResolver{users: m} } func (m *mockUserResolver) GetUserKey(account, user string) (*keys.PublicKey, error) { key, ok := m.users[account+"/"+user] if !ok { return nil, errors.New("not found") } return key, nil } func TestConverters(t *testing.T) { namespace := "root" userName := "JohnDoe" user := namespace + "/" + userName principal := "arn:aws:iam::" + namespace + ":user/" + userName resource := "DOC-EXAMPLE-BUCKET/*" action := "PutObject" mockResolver := newMockUserResolver(t, []string{user}) 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 := &policyengine.Chain{Rules: []policyengine.Rule{ { Status: policyengine.Allow, Actions: policyengine.Actions{Names: []string{action}}, Resources: policyengine.Resources{Names: []string{resource}}, Any: true, Condition: []policyengine.Condition{ { Op: policyengine.CondStringEquals, Object: policyengine.ObjectRequest, Key: RequestOwnerProperty, Value: mockResolver.users[user].Address(), }, { Op: policyengine.CondStringEquals, Object: policyengine.ObjectRequest, Key: "s3:RequestObjectTag/Department", Value: "Finance", }, }, }, }} chain, err := p.ToChain(S3ChainType, mockResolver) require.NoError(t, err) require.Equal(t, expected, chain) }) 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 := &policyengine.Chain{Rules: []policyengine.Rule{ { Status: policyengine.Allow, Actions: policyengine.Actions{Names: []string{"native:" + action}}, Resources: policyengine.Resources{Names: []string{"native:::object/*"}}, Any: true, Condition: []policyengine.Condition{ { Op: policyengine.CondStringEquals, Object: policyengine.ObjectRequest, Key: RequestOwnerProperty, Value: mockResolver.users[user].Address(), }, { Op: policyengine.CondStringLike, Object: policyengine.ObjectResource, Key: ResourceFullPathProperty, Value: resource, }, }, }, }} chain, err := p.ToChain(NativeChainType, mockResolver) require.NoError(t, err) require.Equal(t, expected, chain) }) 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 := &policyengine.Chain{Rules: []policyengine.Rule{ { Status: policyengine.AccessDenied, Actions: policyengine.Actions{Inverted: true, Names: []string{action}}, Resources: policyengine.Resources{Inverted: true, Names: []string{resource}}, Any: true, Condition: []policyengine.Condition{ { Op: policyengine.CondStringNotEquals, Object: policyengine.ObjectRequest, Key: RequestOwnerProperty, Value: mockResolver.users[user].Address(), }, }, }, }} chain, err := p.ToChain(S3ChainType, mockResolver) require.NoError(t, err) require.Equal(t, expected, chain) }) 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:::" + resource}, }}, } expected := &policyengine.Chain{Rules: []policyengine.Rule{ { Status: policyengine.AccessDenied, Actions: policyengine.Actions{Inverted: true, Names: actionToOpMap["GetObject"]}, Resources: policyengine.Resources{Inverted: true, Names: []string{"native:::object/*"}}, Any: true, Condition: []policyengine.Condition{ { Op: policyengine.CondStringEquals, Object: policyengine.ObjectRequest, Key: RequestOwnerProperty, Value: mockResolver.users[user].Address(), }, { Op: policyengine.CondStringLike, Object: policyengine.ObjectResource, Key: ResourceFullPathProperty, Value: resource, }, }, }, }} chain, err := p.ToChain(NativeChainType, mockResolver) require.NoError(t, err) require.Equal(t, expected, chain) }) 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 := p.ToChain(S3ChainType, 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 := p.ToChain(S3ChainType, 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 := p.ToChain(NativeChainType, mockResolver) require.Error(t, err) }) t.Run("check policy conditions", func(t *testing.T) { p := Policy{ Version: "2012-10-17", Statement: []Statement{{ Principal: map[PrincipalType][]string{Wildcard: nil}, Effect: AllowEffect, Action: []string{"s3:PutObject"}, Resource: []string{"arn:aws:s3:::" + resource}, 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"}}, }, }}, } expected := &policyengine.Chain{Rules: []policyengine.Rule{ { Status: policyengine.Allow, Actions: policyengine.Actions{Names: []string{action}}, Resources: policyengine.Resources{Names: []string{resource}}, Any: true, Condition: []policyengine.Condition{ { Op: policyengine.CondStringLike, Object: policyengine.ObjectRequest, Key: RequestOwnerProperty, Value: "*", }, { Op: policyengine.CondStringEquals, Object: policyengine.ObjectRequest, Key: "key1", Value: "val0", }, { Op: policyengine.CondStringEquals, Object: policyengine.ObjectRequest, Key: "key1", Value: "val1", }, { Op: policyengine.CondStringNotEquals, Object: policyengine.ObjectRequest, Key: "key2", Value: "val2", }, { Op: policyengine.CondStringEqualsIgnoreCase, Object: policyengine.ObjectRequest, Key: "key3", Value: "val3", }, { Op: policyengine.CondStringNotEqualsIgnoreCase, Object: policyengine.ObjectRequest, Key: "key4", Value: "val4", }, { Op: policyengine.CondStringLike, Object: policyengine.ObjectRequest, Key: "key5", Value: "val5", }, { Op: policyengine.CondStringNotLike, Object: policyengine.ObjectRequest, Key: "key6", Value: "val6", }, { Op: policyengine.CondStringEquals, Object: policyengine.ObjectRequest, Key: "key7", Value: "1136189045", }, { Op: policyengine.CondStringNotEquals, Object: policyengine.ObjectRequest, Key: "key8", Value: "1136214245", }, { Op: policyengine.CondStringLessThan, Object: policyengine.ObjectRequest, Key: "key9", Value: "1136192645", }, { Op: policyengine.CondStringLessThanEquals, Object: policyengine.ObjectRequest, Key: "key10", Value: "1136203445", }, { Op: policyengine.CondStringGreaterThan, Object: policyengine.ObjectRequest, Key: "key11", Value: "1136217845", }, { Op: policyengine.CondStringGreaterThanEquals, Object: policyengine.ObjectRequest, Key: "key12", Value: "1136225045", }, { Op: policyengine.CondStringEqualsIgnoreCase, Object: policyengine.ObjectRequest, Key: "key13", Value: "True", }, { Op: policyengine.CondStringLike, Object: policyengine.ObjectRequest, Key: "key14", Value: "val14", }, { Op: policyengine.CondStringNotLike, Object: policyengine.ObjectRequest, Key: "key15", Value: "val15", }, { Op: policyengine.CondStringEquals, Object: policyengine.ObjectRequest, Key: "key16", Value: "val16", }, { Op: policyengine.CondStringLike, Object: policyengine.ObjectRequest, Key: RequestOwnerProperty, Value: mockResolver.users[user].Address(), }, { Op: policyengine.CondStringNotEquals, Object: policyengine.ObjectRequest, Key: "key18", Value: "val18", }, { Op: policyengine.CondStringNotLike, Object: policyengine.ObjectRequest, Key: "key19", Value: "val19", }, }, }, }} chain, err := p.ToChain(S3ChainType, mockResolver) require.NoError(t, err) for i, rule := range chain.Rules { expectedRule := expected.Rules[i] require.Equal(t, expectedRule.Actions, rule.Actions) require.Equal(t, expectedRule.Any, rule.Any) require.Equal(t, expectedRule.Resources, rule.Resources) require.Equal(t, expectedRule.Status, rule.Status) require.ElementsMatch(t, expectedRule.Condition, rule.Condition) } }) } 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) }) } }