package parser import ( "encoding/json" "fmt" "sort" "strings" ) // A Policy is a policy made up of multiple allow or deny rules. type Policy struct { Rules []Rule } // MarshalJSON marshals the policy as JSON. func (p *Policy) MarshalJSON() ([]byte, error) { return json.Marshal(p.ToJSON()) } // String converts the policy to a string. func (p *Policy) String() string { str, _ := p.MarshalJSON() return string(str) } // ToJSON converts the policy to JSON. func (p *Policy) ToJSON() Value { var root Array for _, r := range p.Rules { root = append(root, r.ToJSON()) } return root } // PolicyFromValue converts a value into a Policy. func PolicyFromValue(v Value) (*Policy, error) { rules, err := RulesFromValue(v) if err != nil { return nil, fmt.Errorf("invalid rules in policy: %w", err) } return &Policy{ Rules: rules, }, nil } // A Rule is a policy rule with a corresponding action ("allow" or "deny"), // and conditionals to determine if the rule matches or not. type Rule struct { Action Action And []Criterion Or []Criterion Not []Criterion Nor []Criterion } // RulesFromValue converts a Value into a slice of Rules. Only Arrays or Objects // are supported. func RulesFromValue(v Value) ([]Rule, error) { switch t := v.(type) { case Array: return RulesFromArray(t) case Object: return RulesFromObject(t) default: return nil, fmt.Errorf("unsupported type for rule: %T", v) } } // RulesFromArray converts an Array into a slice of Rules. Each element of the Array is // converted using RulesFromObject and merged together. func RulesFromArray(a Array) ([]Rule, error) { var rules []Rule for _, v := range a { switch t := v.(type) { case Object: inner, err := RulesFromObject(t) if err != nil { return nil, err } rules = append(rules, inner...) default: return nil, fmt.Errorf("unsupported type for rules array: %T", v) } } return rules, nil } // RulesFromObject converts an Object into a slice of Rules. // // One form is supported: // // 1. An object where the keys are the actions and the values are an object with "and", "or", or "not" fields: // `{ "allow": { "and": [ {"groups": "group1"} ] } }` func RulesFromObject(o Object) ([]Rule, error) { var rules []Rule for k, v := range o { action, err := ActionFromValue(String(k)) if err != nil { return nil, fmt.Errorf("invalid action in rule: %w", err) } oo, ok := v.(Object) if !ok { return nil, fmt.Errorf("invalid value for action in rule, expected Object, got %T", v) } rule := Rule{ Action: action, } err = rule.fillConditionalsFromObject(oo) if err != nil { return nil, err } rules = append(rules, rule) } // sort by action for deterministic ordering sort.Slice(rules, func(i, j int) bool { return rules[i].Action < rules[j].Action }) return rules, nil } // MarshalJSON marshals the rule as JSON. func (r *Rule) MarshalJSON() ([]byte, error) { return json.Marshal(r.ToJSON()) } // String converts the rule to a string. func (r *Rule) String() string { str, _ := r.MarshalJSON() return string(str) } // ToJSON converts the rule to JSON. func (r *Rule) ToJSON() Value { body := Object{} for _, op := range []struct { operator string criteria []Criterion }{ {"and", r.And}, {"or", r.Or}, {"not", r.Not}, {"nor", r.Nor}, } { if len(op.criteria) == 0 { continue } var criteria Array for _, c := range op.criteria { criteria = append(criteria, c.ToJSON()) } body[op.operator] = criteria } return Object{ string(r.Action): body, } } func (r *Rule) fillConditionalsFromObject(o Object) error { conditionals := []struct { Name string Criteria *[]Criterion }{ {"and", &r.And}, {"or", &r.Or}, {"not", &r.Not}, {"nor", &r.Nor}, } for _, cond := range conditionals { if rawCriteria, ok := o[cond.Name]; ok { criteria, err := CriteriaFromValue(rawCriteria) if err != nil { return fmt.Errorf("invalid criteria in \"%s\"): %w", cond.Name, err) } *cond.Criteria = criteria } } for k := range o { switch k { case "and", "or", "not", "nor", "action": default: return fmt.Errorf("unsupported conditional \"%s\", only and, or, not, nor and action are allowed", k) } } return nil } // A Criterion is used by a rule to determine if the rule matches or not. // // Criteria RegoRulesGenerators are registered based on the specified name. // Data is arbitrary JSON data sent to the generator. type Criterion struct { Name string SubPath string Data Value } // CriteriaFromValue converts a Value into Criteria. Only Arrays are supported. func CriteriaFromValue(v Value) ([]Criterion, error) { switch t := v.(type) { case Array: return CriteriaFromArray(t) default: return nil, fmt.Errorf("unsupported type for criteria: %T", v) } } // CriteriaFromArray converts an Array into Criteria. Each element of the Array is // converted using CriterionFromObject. func CriteriaFromArray(a Array) ([]Criterion, error) { var criteria []Criterion for _, v := range a { switch t := v.(type) { case Object: inner, err := CriterionFromObject(t) if err != nil { return nil, err } criteria = append(criteria, *inner) default: return nil, fmt.Errorf("unsupported type for criteria array: %T", v) } } return criteria, nil } // CriterionFromObject converts an Object into a Criterion. // // One form is supported: // // 1. An object where the keys are the names with a sub path and the values are the corresponding // data for each Criterion: `{ "groups": "group1" }` func CriterionFromObject(o Object) (*Criterion, error) { if len(o) != 1 { return nil, fmt.Errorf("each criteria may only contain a single key and value") } for k, v := range o { name := k subPath := "" if idx := strings.Index(k, "/"); idx >= 0 { name, subPath = k[:idx], k[idx+1:] } return &Criterion{ Name: name, SubPath: subPath, Data: v, }, nil } // this can't happen panic("each criteria may only contain a single key and value") } // MarshalJSON marshals the criterion as JSON. func (c *Criterion) MarshalJSON() ([]byte, error) { return json.Marshal(c.ToJSON()) } // String converts the criterion to a string. func (c *Criterion) String() string { str, _ := c.MarshalJSON() return string(str) } // ToJSON converts the criterion to JSON. func (c *Criterion) ToJSON() Value { nm := c.Name if c.SubPath != "" { nm += "/" + c.SubPath } return Object{nm: c.Data} } // An Action describe what to do when a rule matches, either "allow" or "deny". type Action string // ActionFromValue converts a Value into an Action. func ActionFromValue(value Value) (Action, error) { s, ok := value.(String) if !ok { return "", fmt.Errorf("unsupported type for action: %T", value) } switch Action(s) { case ActionAllow: return ActionAllow, nil case ActionDeny: return ActionDeny, nil } return "", fmt.Errorf("unsupported action: %s", s) } // Actions const ( ActionAllow Action = "allow" ActionDeny Action = "deny" )