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 }