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 ) 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.Kind { case KindResource: val = req.Resource().Property(c.Key) case KindRequest: val = 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 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 }