forked from TrueCloudLab/policy-engine
ab67b9028e
* Introduce new operation with suffix "IfExists" for all string and numeric condition operations; * An operation with "IfExists" suffix specifies the following: if the condition key is present in the context of the request, process the key as specified in the policy. If the key is not present, evaluate the condition element as true; * Change the signature for interface method `Property()`: since it returns two values - the second indicates whether the property exists; * This also means that original condition operations for string and number comparison is slightly changed: if property doesn't exist, then the condition is not evaluated. Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
377 lines
10 KiB
Go
377 lines
10 KiB
Go
package chain
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"strings"
|
|
|
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
|
|
"git.frostfs.info/TrueCloudLab/policy-engine/util"
|
|
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
// ID is the ID of rule chain.
|
|
type ID []byte
|
|
|
|
// MatchType is the match type for chain rules.
|
|
type MatchType uint8
|
|
|
|
const (
|
|
// MatchTypeDenyPriority rejects the request if any `Deny` is specified.
|
|
MatchTypeDenyPriority MatchType = 0
|
|
// MatchTypeFirstMatch returns the first rule action matched to the request.
|
|
MatchTypeFirstMatch MatchType = 1
|
|
)
|
|
|
|
//easyjson:json
|
|
type Chain struct {
|
|
ID ID
|
|
|
|
Rules []Rule
|
|
|
|
MatchType MatchType
|
|
}
|
|
|
|
func (c *Chain) Bytes() []byte {
|
|
data, err := c.MarshalBinary()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func (c *Chain) DecodeBytes(b []byte) error {
|
|
return c.UnmarshalBinary(b)
|
|
}
|
|
|
|
type Rule struct {
|
|
Status Status
|
|
// Actions the operation is applied to.
|
|
Actions Actions
|
|
// List of the resources the operation is applied to.
|
|
Resources Resources
|
|
// True iff individual conditions must be combined with the logical OR.
|
|
// By default AND is used, so _each_ condition must pass.
|
|
Any bool
|
|
Condition []Condition
|
|
}
|
|
|
|
type Actions struct {
|
|
Inverted bool
|
|
Names []string
|
|
}
|
|
|
|
type Resources struct {
|
|
Inverted bool
|
|
Names []string
|
|
}
|
|
|
|
type Condition struct {
|
|
Op ConditionType
|
|
Kind ConditionKindType
|
|
Key string
|
|
Value string
|
|
}
|
|
|
|
type ConditionKindType byte
|
|
|
|
const (
|
|
KindResource ConditionKindType = iota
|
|
KindRequest
|
|
)
|
|
|
|
type ConditionType byte
|
|
|
|
// TODO @fyrchik: reduce the number of conditions.
|
|
// Everything from here should be expressable, but we do not need them all.
|
|
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
|
|
const (
|
|
// String condition operators.
|
|
CondStringEquals ConditionType = iota
|
|
CondStringNotEquals
|
|
CondStringEqualsIgnoreCase
|
|
CondStringNotEqualsIgnoreCase
|
|
CondStringLike
|
|
CondStringNotLike
|
|
CondStringLessThan
|
|
CondStringLessThanEquals
|
|
CondStringGreaterThan
|
|
CondStringGreaterThanEquals
|
|
|
|
// Numeric condition operators.
|
|
CondNumericEquals
|
|
CondNumericNotEquals
|
|
CondNumericLessThan
|
|
CondNumericLessThanEquals
|
|
CondNumericGreaterThan
|
|
CondNumericGreaterThanEquals
|
|
|
|
CondSliceContains
|
|
|
|
CondIPAddress
|
|
CondNotIPAddress
|
|
|
|
CondStringEqualsIfExists
|
|
CondStringEqualsIgnoreCaseIfExists
|
|
CondStringLikeIfExists
|
|
CondStringLessThanIfExists
|
|
CondStringLessThanEqualsIfExists
|
|
CondStringGreaterThanIfExists
|
|
CondStringGreaterThanEqualsIfExists
|
|
|
|
CondNumericEqualsIfExists
|
|
CondNumericLessThanIfExists
|
|
CondNumericLessThanEqualsIfExists
|
|
CondNumericGreaterThanIfExists
|
|
CondNumericGreaterThanEqualsIfExists
|
|
)
|
|
|
|
var condToStr = []struct {
|
|
ct ConditionType
|
|
str string
|
|
}{
|
|
{CondStringEquals, "StringEquals"},
|
|
{CondStringNotEquals, "StringNotEquals"},
|
|
{CondStringEqualsIgnoreCase, "StringEqualsIgnoreCase"},
|
|
{CondStringNotEqualsIgnoreCase, "StringNotEqualsIgnoreCase"},
|
|
{CondStringLike, "StringLike"},
|
|
{CondStringNotLike, "StringNotLike"},
|
|
{CondStringLessThan, "StringLessThan"},
|
|
{CondStringLessThanEquals, "StringLessThanEquals"},
|
|
{CondStringGreaterThan, "StringGreaterThan"},
|
|
{CondStringGreaterThanEquals, "StringGreaterThanEquals"},
|
|
{CondStringEqualsIfExists, "StringEqualsIfExists"},
|
|
{CondStringEqualsIgnoreCaseIfExists, "StringEqualsIgnoreCaseIfExists"},
|
|
{CondStringLikeIfExists, "StringLikeIfExists"},
|
|
{CondStringLessThanIfExists, "StringLessThanIfExists"},
|
|
{CondStringLessThanEqualsIfExists, "StringLessThanEqualsIfExists"},
|
|
{CondStringGreaterThanIfExists, "StringGreaterThanIfExists"},
|
|
{CondStringGreaterThanEqualsIfExists, "StringGreaterThanEqualsIfExists"},
|
|
{CondNumericEquals, "NumericEquals"},
|
|
{CondNumericNotEquals, "NumericNotEquals"},
|
|
{CondNumericLessThan, "NumericLessThan"},
|
|
{CondNumericLessThanEquals, "NumericLessThanEquals"},
|
|
{CondNumericGreaterThan, "NumericGreaterThan"},
|
|
{CondNumericGreaterThanEquals, "NumericGreaterThanEquals"},
|
|
{CondNumericEqualsIfExists, "NumericEqualsIfExists"},
|
|
{CondNumericLessThanIfExists, "NumericLessThanIfExists"},
|
|
{CondNumericLessThanEqualsIfExists, "NumericLessThanEqualsIfExists"},
|
|
{CondNumericGreaterThanIfExists, "NumericGreaterThanIfExists"},
|
|
{CondNumericGreaterThanEqualsIfExists, "NumericGreaterThanEqualsIfExists"},
|
|
{CondSliceContains, "SliceContains"},
|
|
{CondIPAddress, "IPAddress"},
|
|
{CondNotIPAddress, "NotIPAddress"},
|
|
}
|
|
|
|
func (c ConditionType) String() string {
|
|
for _, v := range condToStr {
|
|
if v.ct == c {
|
|
return v.str
|
|
}
|
|
}
|
|
return "unknown condition type"
|
|
}
|
|
|
|
const condSliceContainsDelimiter = "\x00"
|
|
|
|
// FormCondSliceContainsValue builds value for ObjectResource or ObjectRequest property
|
|
// that can be matched by CondSliceContains condition.
|
|
func FormCondSliceContainsValue(values []string) string {
|
|
return strings.Join(values, condSliceContainsDelimiter)
|
|
}
|
|
|
|
func (c *Condition) Match(req resource.Request) bool {
|
|
var val string
|
|
var exists bool
|
|
switch c.Kind {
|
|
case KindResource:
|
|
val, exists = req.Resource().Property(c.Key)
|
|
case KindRequest:
|
|
val, exists = req.Property(c.Key)
|
|
default:
|
|
panic(fmt.Sprintf("unknown condition type: %d", c.Kind))
|
|
}
|
|
|
|
switch c.Op {
|
|
default:
|
|
panic(fmt.Sprintf("unimplemented: %d", c.Op))
|
|
case CondStringEquals:
|
|
return exists && val == c.Value
|
|
case CondStringEqualsIfExists:
|
|
return !exists || val == c.Value
|
|
case CondStringNotEquals:
|
|
return exists && val != c.Value
|
|
case CondStringEqualsIgnoreCase:
|
|
return exists && strings.EqualFold(val, c.Value)
|
|
case CondStringEqualsIgnoreCaseIfExists:
|
|
return !exists || strings.EqualFold(val, c.Value)
|
|
case CondStringNotEqualsIgnoreCase:
|
|
return exists && !strings.EqualFold(val, c.Value)
|
|
case CondStringLike:
|
|
return exists && util.GlobMatch(val, c.Value)
|
|
case CondStringLikeIfExists:
|
|
return !exists || util.GlobMatch(val, c.Value)
|
|
case CondStringNotLike:
|
|
return exists && !util.GlobMatch(val, c.Value)
|
|
case CondStringLessThan:
|
|
return exists && val < c.Value
|
|
case CondStringLessThanIfExists:
|
|
return !exists || val < c.Value
|
|
case CondStringLessThanEquals:
|
|
return exists && val <= c.Value
|
|
case CondStringLessThanEqualsIfExists:
|
|
return !exists || val <= c.Value
|
|
case CondStringGreaterThan:
|
|
return exists && val > c.Value
|
|
case CondStringGreaterThanIfExists:
|
|
return !exists || val > c.Value
|
|
case CondStringGreaterThanEquals:
|
|
return exists && val >= c.Value
|
|
case CondStringGreaterThanEqualsIfExists:
|
|
return !exists || val >= c.Value
|
|
case CondSliceContains:
|
|
return slices.Contains(strings.Split(val, condSliceContainsDelimiter), c.Value)
|
|
case CondNumericEquals, CondNumericNotEquals, CondNumericLessThan, CondNumericLessThanEquals, CondNumericGreaterThan,
|
|
CondNumericGreaterThanEquals:
|
|
return exists && c.matchNumeric(val)
|
|
case CondNumericEqualsIfExists, CondNumericLessThanIfExists, CondNumericLessThanEqualsIfExists, CondNumericGreaterThanIfExists,
|
|
CondNumericGreaterThanEqualsIfExists:
|
|
return !exists || c.matchNumeric(val)
|
|
case CondIPAddress, CondNotIPAddress:
|
|
return c.matchIP(val)
|
|
}
|
|
}
|
|
|
|
func (c *Condition) matchNumeric(val string) bool {
|
|
valDecimal, err := fixedn.Fixed8FromString(val)
|
|
if err != nil {
|
|
return c.Op == CondNumericNotEquals
|
|
}
|
|
|
|
condVal, err := fixedn.Fixed8FromString(c.Value)
|
|
if err != nil {
|
|
return c.Op == CondNumericNotEquals
|
|
}
|
|
|
|
switch c.Op {
|
|
default:
|
|
panic(fmt.Sprintf("unimplemented: %d", c.Op))
|
|
case CondNumericEquals, CondNumericEqualsIfExists:
|
|
return valDecimal.Equal(condVal)
|
|
case CondNumericNotEquals:
|
|
return !valDecimal.Equal(condVal)
|
|
case CondNumericLessThan, CondNumericLessThanIfExists:
|
|
return valDecimal.LessThan(condVal)
|
|
case CondNumericLessThanEquals, CondNumericLessThanEqualsIfExists:
|
|
return valDecimal.LessThan(condVal) || valDecimal.Equal(condVal)
|
|
case CondNumericGreaterThan, CondNumericGreaterThanIfExists:
|
|
return valDecimal.GreaterThan(condVal)
|
|
case CondNumericGreaterThanEquals, CondNumericGreaterThanEqualsIfExists:
|
|
return valDecimal.GreaterThan(condVal) || valDecimal.Equal(condVal)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
if util.GlobMatch(req.Resource().Name(), r.Resources.Names[i]) != r.Resources.Inverted {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return NoRuleFound, false
|
|
}
|
|
for i := range r.Actions.Names {
|
|
if util.GlobMatch(req.Operation(), r.Actions.Names[i]) != r.Actions.Inverted {
|
|
return r.matchCondition(req)
|
|
}
|
|
}
|
|
return NoRuleFound, false
|
|
}
|
|
|
|
func (r *Rule) matchCondition(obj resource.Request) (status Status, matched bool) {
|
|
if r.Any {
|
|
return r.matchAny(obj)
|
|
}
|
|
return r.matchAll(obj)
|
|
}
|
|
|
|
func (r *Rule) matchAny(obj resource.Request) (status Status, matched bool) {
|
|
for i := range r.Condition {
|
|
if r.Condition[i].Match(obj) {
|
|
return r.Status, true
|
|
}
|
|
}
|
|
return NoRuleFound, false
|
|
}
|
|
|
|
func (r *Rule) matchAll(obj resource.Request) (status Status, matched bool) {
|
|
for i := range r.Condition {
|
|
if !r.Condition[i].Match(obj) {
|
|
return NoRuleFound, false
|
|
}
|
|
}
|
|
return r.Status, true
|
|
}
|
|
|
|
func (c *Chain) Match(req resource.Request) (status Status, matched bool) {
|
|
switch c.MatchType {
|
|
case MatchTypeDenyPriority:
|
|
return c.denyPriority(req)
|
|
case MatchTypeFirstMatch:
|
|
return c.firstMatch(req)
|
|
default:
|
|
panic(fmt.Sprintf("unknown MatchType %d", c.MatchType))
|
|
}
|
|
}
|
|
|
|
func (c *Chain) firstMatch(req resource.Request) (status Status, matched bool) {
|
|
for i := range c.Rules {
|
|
status, matched := c.Rules[i].Match(req)
|
|
if matched {
|
|
return status, true
|
|
}
|
|
}
|
|
return NoRuleFound, false
|
|
}
|
|
|
|
func (c *Chain) denyPriority(req resource.Request) (status Status, matched bool) {
|
|
var allowFound bool
|
|
for i := range c.Rules {
|
|
status, matched := c.Rules[i].Match(req)
|
|
if !matched {
|
|
continue
|
|
}
|
|
if status != Allow {
|
|
return status, true
|
|
}
|
|
allowFound = true
|
|
}
|
|
if allowFound {
|
|
return Allow, true
|
|
}
|
|
return NoRuleFound, false
|
|
}
|