package chain import ( "encoding/json" "fmt" "strings" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" "git.frostfs.info/TrueCloudLab/policy-engine/util" "golang.org/x/exp/slices" ) // ID is the ID of rule chain. type ID string // 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 ) type Chain struct { ID ID Rules []Rule MatchType MatchType } func (id ID) MarshalJSON() ([]byte, error) { return json.Marshal([]byte(id)) } func (id *ID) UnmarshalJSON(data []byte) error { var idRaw []byte if err := json.Unmarshal(data, &idRaw); err != nil { return err } *id = ID(idRaw) return nil } func (c *Chain) Bytes() []byte { data, err := json.Marshal(c) if err != nil { panic(err) } return data } func (c *Chain) DecodeBytes(b []byte) error { return json.Unmarshal(b, c) } 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 ) func (c ConditionType) String() string { switch c { case CondStringEquals: return "StringEquals" case CondStringNotEquals: return "StringNotEquals" case CondStringEqualsIgnoreCase: return "StringEqualsIgnoreCase" case CondStringNotEqualsIgnoreCase: return "StringNotEqualsIgnoreCase" case CondStringLike: return "StringLike" case CondStringNotLike: return "StringNotLike" case CondStringLessThan: return "StringLessThan" case CondStringLessThanEquals: return "StringLessThanEquals" case CondStringGreaterThan: return "StringGreaterThan" case CondStringGreaterThanEquals: return "StringGreaterThanEquals" case CondNumericEquals: return "NumericEquals" case CondNumericNotEquals: return "NumericNotEquals" case CondNumericLessThan: return "NumericLessThan" case CondNumericLessThanEquals: return "NumericLessThanEquals" case CondNumericGreaterThan: return "NumericGreaterThan" case CondNumericGreaterThanEquals: return "NumericGreaterThanEquals" case CondSliceContains: return "SliceContains" default: 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) } } 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 }