package policyengine import ( "encoding/json" "fmt" "strings" ) // Engine ... type Engine interface { // IsAllowed returns status for the operation after all checks. // The second return value signifies whether a matching rule was found. IsAllowed(name Name, namespace string, r Request) (Status, bool) } // ChainID is the ID of rule chain. type ChainID string type Chain struct { ID ChainID Rules []Rule } 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. Action []string // List of the resources the operation is applied to. Resource []string // 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 Condition struct { Op ConditionType Object ObjectType Key string Value string } type ObjectType byte const ( ObjectResource ObjectType = iota ObjectRequest ObjectActor ) 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 ) func (c *Condition) Match(req 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: %s", 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 globMatch(val, c.Value) case CondStringNotLike: return !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 } } func (r *Rule) Match(req Request) (status Status, matched bool) { found := len(r.Resource) == 0 for i := range r.Resource { if globMatch(req.Resource().Name(), r.Resource[i]) { found = true break } } if !found { return NoRuleFound, false } for i := range r.Action { if globMatch(req.Operation(), r.Action[i]) { return r.matchCondition(req) } } return NoRuleFound, false } func (r *Rule) matchCondition(obj Request) (status Status, matched bool) { if r.Any { return r.matchAny(obj) } return r.matchAll(obj) } func (r *Rule) matchAny(obj 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 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 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 }