forked from TrueCloudLab/policy-engine
[#67] chain: Support IPAddress conditions
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
parent
0e69e48511
commit
ff5d05ac92
4 changed files with 167 additions and 24 deletions
|
@ -3,6 +3,7 @@ package iam
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -66,6 +67,7 @@ const (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
condKeyAWSPrincipalARN = "aws:PrincipalArn"
|
condKeyAWSPrincipalARN = "aws:PrincipalArn"
|
||||||
|
condKeyAWSSourceIP = "aws:SourceIp"
|
||||||
condKeyAWSPrincipalTagPrefix = "aws:PrincipalTag/"
|
condKeyAWSPrincipalTagPrefix = "aws:PrincipalTag/"
|
||||||
userClaimTagPrefix = "tag-"
|
userClaimTagPrefix = "tag-"
|
||||||
)
|
)
|
||||||
|
@ -198,6 +200,11 @@ func transformKey(key string) string {
|
||||||
return fmt.Sprintf(common.PropertyKeyFormatFrostFSIDUserClaim, userClaimTagPrefix+tagName)
|
return fmt.Sprintf(common.PropertyKeyFormatFrostFSIDUserClaim, userClaimTagPrefix+tagName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case condKeyAWSSourceIP:
|
||||||
|
return common.PropertyKeyFrostFSSourceIP
|
||||||
|
}
|
||||||
|
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,13 +262,9 @@ func getConditionTypeAndConverter(op string) (chain.ConditionType, convertFuncti
|
||||||
case op == CondBool:
|
case op == CondBool:
|
||||||
return chain.CondStringEqualsIgnoreCase, noConvertFunction, nil
|
return chain.CondStringEqualsIgnoreCase, noConvertFunction, nil
|
||||||
case op == CondIPAddress:
|
case op == CondIPAddress:
|
||||||
// todo consider using converters
|
return chain.CondIPAddress, ipConvertFunction, nil
|
||||||
// "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
|
|
||||||
case op == CondNotIPAddress:
|
case op == CondNotIPAddress:
|
||||||
return chain.CondStringNotLike, noConvertFunction, nil
|
return chain.CondNotIPAddress, ipConvertFunction, nil
|
||||||
case op == CondSliceContains:
|
case op == CondSliceContains:
|
||||||
return chain.CondSliceContains, noConvertFunction, nil
|
return chain.CondSliceContains, noConvertFunction, nil
|
||||||
default:
|
default:
|
||||||
|
@ -302,6 +305,25 @@ func numericConvertFunction(val string) (string, error) {
|
||||||
return "", fmt.Errorf("invalid numeric value: '%s'", val)
|
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) {
|
func dateConvertFunction(val string) (string, error) {
|
||||||
if _, err := strconv.ParseInt(val, 10, 64); err == nil {
|
if _, err := strconv.ParseInt(val, 10, 64); err == nil {
|
||||||
return val, nil
|
return val, nil
|
||||||
|
|
|
@ -391,8 +391,6 @@ func TestConvertToChainCondition(t *testing.T) {
|
||||||
CondDateGreaterThan: {"key11": {"2006-01-02T15:04:05-01:00"}},
|
CondDateGreaterThan: {"key11": {"2006-01-02T15:04:05-01:00"}},
|
||||||
CondDateGreaterThanEquals: {"key12": {"2006-01-02T15:04:05-03:00"}},
|
CondDateGreaterThanEquals: {"key12": {"2006-01-02T15:04:05-03:00"}},
|
||||||
CondBool: {"key13": {"True"}},
|
CondBool: {"key13": {"True"}},
|
||||||
CondIPAddress: {"key14": {"val14"}},
|
|
||||||
CondNotIPAddress: {"key15": {"val15"}},
|
|
||||||
CondArnEquals: {"key16": {"val16"}},
|
CondArnEquals: {"key16": {"val16"}},
|
||||||
CondArnLike: {condKeyAWSPrincipalARN: {principal}},
|
CondArnLike: {condKeyAWSPrincipalARN: {principal}},
|
||||||
CondArnNotEquals: {"key18": {"val18"}},
|
CondArnNotEquals: {"key18": {"val18"}},
|
||||||
|
@ -519,22 +517,6 @@ func TestConvertToChainCondition(t *testing.T) {
|
||||||
Value: "True",
|
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{{
|
Conditions: []chain.Condition{{
|
||||||
Op: chain.CondStringEquals,
|
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) {
|
func TestParsePrincipalARN(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for i, tc := range []struct {
|
||||||
principal string
|
principal string
|
||||||
|
|
|
@ -2,6 +2,7 @@ package chain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
|
||||||
|
@ -109,6 +110,9 @@ const (
|
||||||
CondNumericGreaterThanEquals
|
CondNumericGreaterThanEquals
|
||||||
|
|
||||||
CondSliceContains
|
CondSliceContains
|
||||||
|
|
||||||
|
CondIPAddress
|
||||||
|
CondNotIPAddress
|
||||||
)
|
)
|
||||||
|
|
||||||
var condToStr = []struct {
|
var condToStr = []struct {
|
||||||
|
@ -132,6 +136,8 @@ var condToStr = []struct {
|
||||||
{CondNumericGreaterThan, "NumericGreaterThan"},
|
{CondNumericGreaterThan, "NumericGreaterThan"},
|
||||||
{CondNumericGreaterThanEquals, "NumericGreaterThanEquals"},
|
{CondNumericGreaterThanEquals, "NumericGreaterThanEquals"},
|
||||||
{CondSliceContains, "SliceContains"},
|
{CondSliceContains, "SliceContains"},
|
||||||
|
{CondIPAddress, "IPAddress"},
|
||||||
|
{CondNotIPAddress, "NotIPAddress"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c ConditionType) String() string {
|
func (c ConditionType) String() string {
|
||||||
|
@ -190,6 +196,8 @@ func (c *Condition) Match(req resource.Request) bool {
|
||||||
case CondNumericEquals, CondNumericNotEquals, CondNumericLessThan, CondNumericLessThanEquals, CondNumericGreaterThan,
|
case CondNumericEquals, CondNumericNotEquals, CondNumericLessThan, CondNumericLessThanEquals, CondNumericGreaterThan,
|
||||||
CondNumericGreaterThanEquals:
|
CondNumericGreaterThanEquals:
|
||||||
return c.matchNumeric(val)
|
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) {
|
func (r *Rule) Match(req resource.Request) (status Status, matched bool) {
|
||||||
found := len(r.Resources.Names) == 0
|
found := len(r.Resources.Names) == 0
|
||||||
for i := range r.Resources.Names {
|
for i := range r.Resources.Names {
|
||||||
|
|
|
@ -3,5 +3,7 @@ package common
|
||||||
const (
|
const (
|
||||||
PropertyKeyFrostFSIDGroupID = "frostfsid:groupID"
|
PropertyKeyFrostFSIDGroupID = "frostfsid:groupID"
|
||||||
|
|
||||||
|
PropertyKeyFrostFSSourceIP = "frostfs:sourceIP"
|
||||||
|
|
||||||
PropertyKeyFormatFrostFSIDUserClaim = "frostfsid:userClaim/%s"
|
PropertyKeyFormatFrostFSIDUserClaim = "frostfsid:userClaim/%s"
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue