From ff5d05ac922b02545ea4ee0ac20b7736de665eee Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Wed, 10 Apr 2024 18:11:07 +0300 Subject: [PATCH] [#67] chain: Support IPAddress conditions Signed-off-by: Denis Kirillov --- iam/converter.go | 34 +++++++++-- iam/converter_test.go | 126 ++++++++++++++++++++++++++++++++++------ pkg/chain/chain.go | 29 +++++++++ schema/common/consts.go | 2 + 4 files changed, 167 insertions(+), 24 deletions(-) diff --git a/iam/converter.go b/iam/converter.go index c81be05..8d29ef4 100644 --- a/iam/converter.go +++ b/iam/converter.go @@ -3,6 +3,7 @@ package iam import ( "errors" "fmt" + "net/netip" "strconv" "strings" "time" @@ -66,6 +67,7 @@ const ( const ( condKeyAWSPrincipalARN = "aws:PrincipalArn" + condKeyAWSSourceIP = "aws:SourceIp" condKeyAWSPrincipalTagPrefix = "aws:PrincipalTag/" userClaimTagPrefix = "tag-" ) @@ -198,6 +200,11 @@ func transformKey(key string) string { return fmt.Sprintf(common.PropertyKeyFormatFrostFSIDUserClaim, userClaimTagPrefix+tagName) } + switch key { + case condKeyAWSSourceIP: + return common.PropertyKeyFrostFSSourceIP + } + return key } @@ -255,13 +262,9 @@ func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFuncti case op == CondBool: return chain.CondStringEqualsIgnoreCase, noConvertFunction, nil case op == CondIPAddress: - // todo consider using converters - // "203.0.113.0/24" -> "203.0.113.*", - // "2001:DB8:1234:5678::/64" -> "2001:DB8:1234:5678:*" - // or having specific condition type for IP - return chain.CondStringLike, noConvertFunction, nil + return chain.CondIPAddress, ipConvertFunction, nil case op == CondNotIPAddress: - return chain.CondStringNotLike, noConvertFunction, nil + return chain.CondNotIPAddress, ipConvertFunction, nil case op == CondSliceContains: return chain.CondSliceContains, noConvertFunction, nil default: @@ -302,6 +305,25 @@ func numericConvertFunction(val string) (string, error) { return "", fmt.Errorf("invalid numeric value: '%s'", val) } +func ipConvertFunction(val string) (string, error) { + var ipAddr netip.Addr + + if prefix, err := netip.ParsePrefix(val); err != nil { + if ipAddr, err = netip.ParseAddr(val); err != nil { + return "", err + } + val += "/32" + } else { + ipAddr = prefix.Addr() + } + + if ipAddr.IsPrivate() { + return "", fmt.Errorf("invalid ip value '%s': must be public", val) + } + + return val, nil +} + func dateConvertFunction(val string) (string, error) { if _, err := strconv.ParseInt(val, 10, 64); err == nil { return val, nil diff --git a/iam/converter_test.go b/iam/converter_test.go index 4526a75..aa7b889 100644 --- a/iam/converter_test.go +++ b/iam/converter_test.go @@ -391,8 +391,6 @@ func TestConvertToChainCondition(t *testing.T) { 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"}}, @@ -519,22 +517,6 @@ func TestConvertToChainCondition(t *testing.T) { 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, @@ -639,6 +621,114 @@ func TestConvertToChainCondition(t *testing.T) { } } +func TestIPConditions(t *testing.T) { + t.Run("ip converters", func(t *testing.T) { + for _, tc := range []struct { + ip string + error bool + expected string + }{ + {ip: "203.0.113.0/24", expected: "203.0.113.0/24"}, + {ip: "203.0.113.1", expected: "203.0.113.1/32"}, + {ip: "203.0.113.1/", error: true}, + {ip: "203.0.113.1/33", error: true}, + {ip: "192.168.0.1/24", error: true}, + {ip: "10.10.0.1/24", error: true}, + {ip: "172.16.0.1/24", error: true}, + {ip: "2001:DB8:1234:5678::/64", expected: "2001:DB8:1234:5678::/64"}, + {ip: "2001:DB8:1234:5678::", expected: "2001:DB8:1234:5678::/32"}, + {ip: "2001:DB8:1234:5678::/", error: true}, + {ip: "2001:DB8:1234:5678::/129", error: true}, + {ip: "FC00::/64", error: true}, + } { + t.Run("", func(t *testing.T) { + actual, err := ipConvertFunction(tc.ip) + if tc.error { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tc.expected, actual) + }) + } + }) + + t.Run("chain converters", func(t *testing.T) { + policy := `{"Version":"2012-10-17", +"Statement":{"Effect":"Allow","Principal": "*","Action":"s3:*","Resource":"*","Condition": {"IpAddress": {"aws:SourceIp": "203.0.113.0/24"}}} +}` + + 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}}, + Condition: []chain.Condition{{ + Op: chain.CondIPAddress, + Object: chain.ObjectRequest, + Key: common.PropertyKeyFrostFSSourceIP, + Value: "203.0.113.0/24", + }}, + }}, + } + + 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}}, + Condition: []chain.Condition{{ + Op: chain.CondIPAddress, + Object: chain.ObjectRequest, + Key: common.PropertyKeyFrostFSSourceIP, + Value: "203.0.113.0/24", + }}, + }}, + } + + nativeChain, err := ConvertToNativeChain(p, newMockUserResolver(nil, nil, "")) + require.NoError(t, err) + require.Equal(t, nativeExpected, nativeChain) + }) + + t.Run("matching rules", func(t *testing.T) { + policy := `{"Version":"2012-10-17", +"Statement":{"Effect":"Allow","Principal": "*","Action":"s3:*","Resource":"*","Condition": {"IpAddress": {"aws:SourceIp": "203.0.113.0/24"}}} +}` + + var p Policy + err := json.Unmarshal([]byte(policy), &p) + require.NoError(t, err) + + s3Chain, err := ConvertToS3Chain(p, newMockUserResolver(nil, nil, "")) + require.NoError(t, err) + + s := inmemory.NewInMemory() + _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, engine.NamespaceTarget(""), s3Chain) + require.NoError(t, err) + + req := testutil.NewRequest("s3:CreateBucket", testutil.NewResource(fmt.Sprintf(s3.ResourceFormatS3Bucket, "bkt"), nil), + map[string]string{common.PropertyKeyFrostFSSourceIP: "203.0.113.128"}) + status, _, err := s.IsAllowed(chain.S3, engine.NewRequestTargetWithNamespace(""), req) + require.NoError(t, err) + require.Equal(t, chain.Allow.String(), status.String()) + + req = testutil.NewRequest("s3:CreateBucket", testutil.NewResource(fmt.Sprintf(s3.ResourceFormatS3Bucket, "bkt"), nil), + map[string]string{common.PropertyKeyFrostFSSourceIP: "203.0.114.0"}) + status, _, err = s.IsAllowed(chain.S3, engine.NewRequestTargetWithNamespace(""), req) + require.NoError(t, err) + require.Equal(t, chain.NoRuleFound.String(), status.String()) + }) +} + func TestParsePrincipalARN(t *testing.T) { for i, tc := range []struct { principal string diff --git a/pkg/chain/chain.go b/pkg/chain/chain.go index 87685e5..ce2d6b0 100644 --- a/pkg/chain/chain.go +++ b/pkg/chain/chain.go @@ -2,6 +2,7 @@ package chain import ( "fmt" + "net/netip" "strings" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" @@ -109,6 +110,9 @@ const ( CondNumericGreaterThanEquals CondSliceContains + + CondIPAddress + CondNotIPAddress ) var condToStr = []struct { @@ -132,6 +136,8 @@ var condToStr = []struct { {CondNumericGreaterThan, "NumericGreaterThan"}, {CondNumericGreaterThanEquals, "NumericGreaterThanEquals"}, {CondSliceContains, "SliceContains"}, + {CondIPAddress, "IPAddress"}, + {CondNotIPAddress, "NotIPAddress"}, } func (c ConditionType) String() string { @@ -190,6 +196,8 @@ func (c *Condition) Match(req resource.Request) bool { case CondNumericEquals, CondNumericNotEquals, CondNumericLessThan, CondNumericLessThanEquals, CondNumericGreaterThan, CondNumericGreaterThanEquals: return c.matchNumeric(val) + case CondIPAddress, CondNotIPAddress: + return c.matchIP(val) } } @@ -222,6 +230,27 @@ func (c *Condition) matchNumeric(val string) bool { } } +func (c *Condition) matchIP(val string) bool { + ipAddr, err := netip.ParseAddr(val) + if err != nil { + return false + } + + prefix, err := netip.ParsePrefix(c.Value) + if err != nil { + return false + } + + switch c.Op { + default: + panic(fmt.Sprintf("unimplemented: %d", c.Op)) + case CondIPAddress: + return prefix.Contains(ipAddr) + case CondNotIPAddress: + return !prefix.Contains(ipAddr) + } +} + func (r *Rule) Match(req resource.Request) (status Status, matched bool) { found := len(r.Resources.Names) == 0 for i := range r.Resources.Names { diff --git a/schema/common/consts.go b/schema/common/consts.go index 592df91..dabd936 100644 --- a/schema/common/consts.go +++ b/schema/common/consts.go @@ -3,5 +3,7 @@ package common const ( PropertyKeyFrostFSIDGroupID = "frostfsid:groupID" + PropertyKeyFrostFSSourceIP = "frostfs:sourceIP" + PropertyKeyFormatFrostFSIDUserClaim = "frostfsid:userClaim/%s" )