generated from TrueCloudLab/basic
e75200bb8e
Signed-off-by: Airat Arifullin <aarifullin@yadro.com>
333 lines
7.9 KiB
Go
333 lines
7.9 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
|
|
Object ObjectType
|
|
Key string
|
|
Value string
|
|
}
|
|
|
|
type ObjectType byte
|
|
|
|
const (
|
|
ObjectResource ObjectType = iota
|
|
ObjectRequest
|
|
)
|
|
|
|
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
|
|
)
|
|
|
|
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"},
|
|
{CondNumericEquals, "NumericEquals"},
|
|
{CondNumericNotEquals, "NumericNotEquals"},
|
|
{CondNumericLessThan, "NumericLessThan"},
|
|
{CondNumericLessThanEquals, "NumericLessThanEquals"},
|
|
{CondNumericGreaterThan, "NumericGreaterThan"},
|
|
{CondNumericGreaterThanEquals, "NumericGreaterThanEquals"},
|
|
{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
|
|
switch c.Object {
|
|
case ObjectResource:
|
|
val = req.Resource().Property(c.Key)
|
|
case ObjectRequest:
|
|
val = req.Property(c.Key)
|
|
default:
|
|
panic(fmt.Sprintf("unknown condition type: %d", c.Object))
|
|
}
|
|
|
|
switch c.Op {
|
|
default:
|
|
panic(fmt.Sprintf("unimplemented: %d", c.Op))
|
|
case CondStringEquals:
|
|
return val == c.Value
|
|
case CondStringNotEquals:
|
|
return val != c.Value
|
|
case CondStringEqualsIgnoreCase:
|
|
return strings.EqualFold(val, c.Value)
|
|
case CondStringNotEqualsIgnoreCase:
|
|
return !strings.EqualFold(val, c.Value)
|
|
case CondStringLike:
|
|
return util.GlobMatch(val, c.Value)
|
|
case CondStringNotLike:
|
|
return !util.GlobMatch(val, c.Value)
|
|
case CondStringLessThan:
|
|
return val < c.Value
|
|
case CondStringLessThanEquals:
|
|
return val <= c.Value
|
|
case CondStringGreaterThan:
|
|
return val > c.Value
|
|
case CondStringGreaterThanEquals:
|
|
return val >= c.Value
|
|
case CondSliceContains:
|
|
return slices.Contains(strings.Split(val, condSliceContainsDelimiter), c.Value)
|
|
case CondNumericEquals, CondNumericNotEquals, CondNumericLessThan, CondNumericLessThanEquals, CondNumericGreaterThan,
|
|
CondNumericGreaterThanEquals:
|
|
return 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:
|
|
return valDecimal.Equal(condVal)
|
|
case CondNumericNotEquals:
|
|
return !valDecimal.Equal(condVal)
|
|
case CondNumericLessThan:
|
|
return valDecimal.LessThan(condVal)
|
|
case CondNumericLessThanEquals:
|
|
return valDecimal.LessThan(condVal) || valDecimal.Equal(condVal)
|
|
case CondNumericGreaterThan:
|
|
return valDecimal.GreaterThan(condVal)
|
|
case CondNumericGreaterThanEquals:
|
|
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
|
|
}
|