generated from TrueCloudLab/basic
[#67] chain: Support IPAddress conditions #67
4 changed files with 167 additions and 24 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -3,5 +3,7 @@ package common
|
|||
const (
|
||||
PropertyKeyFrostFSIDGroupID = "frostfsid:groupID"
|
||||
|
||||
PropertyKeyFrostFSSourceIP = "frostfs:sourceIP"
|
||||
|
||||
PropertyKeyFormatFrostFSIDUserClaim = "frostfsid:userClaim/%s"
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue
Consider using
netip
package, it should be more optimized (ParseCIDR
isnetip.ParsePrefix
, other functions also should be present there).