[#67] chain: Support IPAddress conditions #67

Merged
dkirillov merged 1 commit from dkirillov/policy-engine:feature/ip_conditions into master 2024-04-15 12:52:42 +00:00
4 changed files with 167 additions and 24 deletions
Showing only changes of commit c146ab165e - Show all commits

View file

@ -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

Consider using netip package, it should be more optimized (ParseCIDR is netip.ParsePrefix, other functions also should be present there).

Consider using `netip` package, it should be more optimized (`ParseCIDR` is `netip.ParsePrefix`, other functions also should be present there).
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

View file

@ -380,8 +380,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"}},
@ -508,22 +506,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,
@ -628,6 +610,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

View file

@ -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 {

View file

@ -3,5 +3,7 @@ package common
const (
PropertyKeyFrostFSIDGroupID = "frostfsid:groupID"
PropertyKeyFrostFSSourceIP = "frostfs:sourceIP"
PropertyKeyFormatFrostFSIDUserClaim = "frostfsid:userClaim/%s"
)