package rulesengine // request payloads represent any datatype being sent to the rules engine. // these will often have a validation method. import ( "errors" "fmt" "github.com/google/uuid" ) var ( ErrInvalidCommandNil = errors.New("command was nil") ErrInvalidBannerID = errors.New("bannerid was not a valid uuid") ) type PostCommandPayload struct { Name string `json:"name"` } func (pc PostCommandPayload) Validate() error { if pc.Name == "" { return fmt.Errorf("empty command name") } return nil } type PostPrivilegePayload struct { Name string `json:"name"` } func (pp PostPrivilegePayload) Validate() error { if pp.Name == "" { return fmt.Errorf("empty privilege name") } return nil } type ValidateCommandPayload struct { Identity Identity `json:"identity"` Command Command `json:"command"` Target Target `json:"target"` } func (vc ValidateCommandPayload) Validate() error { if vc.Command.Name == "" { return ErrInvalidCommandNil } if vc.Command.Type == "" { // Only check for emptiness - the db may be aware of valid request types // that we are not yet aware of. If a request type is supplied which the // db is not aware of, this is considered an error and the db will // return an error return errors.New("command type was empty") } _, err := uuid.Parse(vc.Target.BannerID) if err != nil { return ErrInvalidBannerID } return nil } type WriteRule struct { Command string `json:"command"` Privileges []string `json:"privileges"` } func (pr WriteRule) Validate() error { if pr.Command == "" { return fmt.Errorf("empty command name") } if len(pr.Privileges) == 0 { return fmt.Errorf("empty privilege list") } for i, privilege := range pr.Privileges { if privilege == "" { return fmt.Errorf("empty privilege name in array at %d", i) } } return nil } type WriteRules []WriteRule func (pr WriteRules) Validate() error { if len(pr) == 0 { return fmt.Errorf("empty rules list") } for i, rule := range pr { if err := rule.Validate(); err != nil { return fmt.Errorf("invalid rule at %d: %w", i, err) } } return nil } type RuleSet struct { Privilege string Commands []string } func (rs RuleSet) Validate() error { if rs.Privilege == "" { return fmt.Errorf("empty privilege name") } if len(rs.Commands) == 0 { return fmt.Errorf("empty command list") } for i, command := range rs.Commands { if command == "" { return fmt.Errorf("empty command name in array at %d", i) } } return nil } type RuleSets []RuleSet func (rs RuleSets) Validate() error { if len(rs) == 0 { return fmt.Errorf("empty rules list") } // Protect database from a massive bulk write attempt // Rather than validating the number of rules, validate against the sum // total number of commands added to each rule. Otherwise it would be // possible to add an unbounded number of commands to a single rule, or add // many commands to many rules, resulting in an insert of size O(n^2). numCommands := 0 for _, ruleSet := range rs { numCommands += len(ruleSet.Commands) } if numCommands > maxRules { return fmt.Errorf("total number of commands in rules %d exceeds max %d", numCommands, maxRules) } var retErr error for i, rule := range rs { if err := rule.Validate(); err != nil { retErr = errors.Join(retErr, fmt.Errorf("invalid rule at %d: %w", i, err)) } } return retErr }