[core] Add config validation command

- Fix missing field validation for required fields
- Add validation of template fields
- Report all issues in configuration

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2022-10-31 17:26:53 +01:00
parent ae7879447e
commit 2c71f57d02
Signed by: luzifer
GPG Key ID: D91C3E91E4CAD6F5
23 changed files with 180 additions and 29 deletions

View File

@ -37,6 +37,7 @@ Supported sub-commands are:
actor-docs Generate markdown documentation for available actors
api-token <name> <scope...> Generate an api-token to be entered into the config
migrate-v2 <old file> Migrate old (*.json.gz) storage file into new database
validate-config Try to load configuration file and report errors if any
help Prints this help message
```

View File

@ -124,10 +124,17 @@ func (a ActorScript) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eve
func (a ActorScript) IsAsync() bool { return false }
func (a ActorScript) Name() string { return "script" }
func (a ActorScript) Validate(attrs *plugins.FieldCollection) (err error) {
if cmd, err := attrs.StringSlice("command"); err != nil || len(cmd) == 0 {
func (a ActorScript) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
cmd, err := attrs.StringSlice("command")
if err != nil || len(cmd) == 0 {
return errors.New("command must be slice of strings with length > 0")
}
for i, el := range cmd {
if err = tplValidator(el); err != nil {
return errors.Wrapf(err, "validating cmd template (element %d)", i)
}
}
return nil
}

View File

@ -357,21 +357,34 @@ func (c *configFile) updateAutoMessagesFromConfig(old *configFile) {
}
func (c configFile) validateRuleActions() error {
var hasError bool
for _, r := range c.Rules {
logger := log.WithField("rule", r.MatcherID())
if err := r.Validate(validateTemplate); err != nil {
logger.WithError(err).Error("Rule reported invalid config")
hasError = true
}
for idx, a := range r.Actions {
actor, err := getActorByName(a.Type)
if err != nil {
logger.WithField("index", idx).WithError(err).Error("Cannot get actor by type")
return errors.Wrap(err, "getting actor by type")
hasError = true
continue
}
if err = actor.Validate(a.Attributes); err != nil {
if err = actor.Validate(validateTemplate, a.Attributes); err != nil {
logger.WithField("index", idx).WithError(err).Error("Actor reported invalid config")
return errors.Wrap(err, "validating action attributes")
hasError = true
}
}
}
if hasError {
return errors.New("config validation reported errors, see log")
}
return nil
}

View File

@ -103,7 +103,18 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { return nil }
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
reasonTemplate, err := attrs.String("reason")
if err != nil || reasonTemplate == "" {
return errors.New("reason must be non-empty string")
}
if err = tplValidator(reasonTemplate); err != nil {
return errors.Wrap(err, "validating reason template")
}
return nil
}
func handleAPIBan(w http.ResponseWriter, r *http.Request) {
var (

View File

@ -187,11 +187,17 @@ func (a ActorCounter) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, ev
func (a ActorCounter) IsAsync() bool { return false }
func (a ActorCounter) Name() string { return "counter" }
func (a ActorCounter) Validate(attrs *plugins.FieldCollection) (err error) {
func (a ActorCounter) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
if cn, err := attrs.String("counter"); err != nil || cn == "" {
return errors.New("counter name must be non-empty string")
}
for _, field := range []string{"counter", "counter_step", "counter_set"} {
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
return errors.Wrapf(err, "validating %s template", field)
}
}
return nil
}

View File

@ -69,4 +69,6 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { return nil }
func (a actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) (err error) {
return nil
}

View File

@ -46,4 +46,6 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { return nil }
func (a actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) (err error) {
return nil
}

View File

@ -106,10 +106,15 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return true }
func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs *plugins.FieldCollection) error {
if v, err := attrs.String("source"); err != nil || v == "" {
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) error {
sourceTpl, err := attrs.String("source")
if err != nil || sourceTpl == "" {
return errors.New("source is expected to be non-empty string")
}
if err = tplValidator(sourceTpl); err != nil {
return errors.Wrap(err, "validating source template")
}
return nil
}

View File

@ -16,6 +16,8 @@ const actorName = "modchannel"
var (
formatMessage plugins.MsgFormatter
tcGetter func(string) (*twitch.Client, error)
ptrStringEmpty = func(s string) *string { return &s }("")
)
func Register(args plugins.RegistrationArguments) error {
@ -67,9 +69,8 @@ type actor struct{}
func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
var (
ptrStringEmpty = func(v string) *string { return &v }("")
game = attrs.MustString("game", ptrStringEmpty)
title = attrs.MustString("title", ptrStringEmpty)
game = attrs.MustString("game", ptrStringEmpty)
title = attrs.MustString("title", ptrStringEmpty)
)
if game == "" && title == "" {
@ -115,10 +116,16 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.String("channel"); err != nil || v == "" {
return errors.New("channel must be non-empty string")
}
for _, field := range []string{"channel", "game", "title"} {
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
return errors.Wrapf(err, "validating %s template", field)
}
}
return nil
}

View File

@ -28,6 +28,7 @@ var (
messageStoreLock sync.RWMutex
ptrStringDelete = func(v string) *string { return &v }("delete")
ptrStringEmpty = func(s string) *string { return &s }("")
ptrString10m = func(v string) *string { return &v }("10m")
)
@ -230,10 +231,16 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.String("match"); err != nil || v == "" {
return errors.New("match must be non-empty string")
}
for _, field := range []string{"scan", "action", "match"} {
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
return errors.Wrapf(err, "validating %s template", field)
}
}
return nil
}

View File

@ -214,7 +214,7 @@ func (a actorPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eve
func (a actorPunish) IsAsync() bool { return false }
func (a actorPunish) Name() string { return actorNamePunish }
func (a actorPunish) Validate(attrs *plugins.FieldCollection) (err error) {
func (a actorPunish) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.String("user"); err != nil || v == "" {
return errors.New("user must be non-empty string")
}
@ -223,6 +223,10 @@ func (a actorPunish) Validate(attrs *plugins.FieldCollection) (err error) {
return errors.New("levels must be slice of strings with length > 0")
}
if err = tplValidator(attrs.MustString("user", ptrStringEmpty)); err != nil {
return errors.Wrap(err, "validating user template")
}
return nil
}
@ -247,10 +251,14 @@ func (a actorResetPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule
func (a actorResetPunish) IsAsync() bool { return false }
func (a actorResetPunish) Name() string { return actorNameResetPunish }
func (a actorResetPunish) Validate(attrs *plugins.FieldCollection) (err error) {
func (a actorResetPunish) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.String("user"); err != nil || v == "" {
return errors.New("user must be non-empty string")
}
if err = tplValidator(attrs.MustString("user", ptrStringEmpty)); err != nil {
return errors.Wrap(err, "validating user template")
}
return nil
}

View File

@ -173,7 +173,7 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
action := attrs.MustString("action", ptrStringEmpty)
switch action {
@ -194,5 +194,11 @@ func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
return errors.New("action must be one of add, del or get")
}
for _, field := range []string{"index", "quote", "format"} {
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
return errors.Wrapf(err, "validating %s template", field)
}
}
return nil
}

View File

@ -12,6 +12,8 @@ const actorName = "raw"
var (
formatMessage plugins.MsgFormatter
send plugins.SendMessageFunc
ptrStringEmpty = func(s string) *string { return &s }("")
)
func Register(args plugins.RegistrationArguments) error {
@ -63,10 +65,14 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.String("message"); err != nil || v == "" {
return errors.New("message must be non-empty string")
}
if err = tplValidator(attrs.MustString("message", ptrStringEmpty)); err != nil {
return errors.Wrap(err, "validating message template")
}
return nil
}

View File

@ -17,7 +17,8 @@ var (
formatMessage plugins.MsgFormatter
send plugins.SendMessageFunc
ptrBoolFalse = func(v bool) *bool { return &v }(false)
ptrBoolFalse = func(v bool) *bool { return &v }(false)
ptrStringEmpty = func(s string) *string { return &s }("")
)
func Register(args plugins.RegistrationArguments) error {
@ -121,10 +122,16 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.String("message"); err != nil || v == "" {
return errors.New("message must be non-empty string")
}
for _, field := range []string{"message", "fallback"} {
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
return errors.Wrapf(err, "validating %s template", field)
}
}
return nil
}

View File

@ -82,11 +82,19 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.Duration("duration"); err != nil || v < time.Second {
return errors.New("duration must be of type duration greater or equal one second")
}
if v, err := attrs.String("reason"); err != nil || v == "" {
return errors.New("reason must be non-empty string")
}
if err = tplValidator(attrs.MustString("reason", ptrStringEmpty)); err != nil {
return errors.Wrap(err, "validating reason template")
}
return nil
}

View File

@ -150,11 +150,17 @@ func (a ActorSetVariable) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule
func (a ActorSetVariable) IsAsync() bool { return false }
func (a ActorSetVariable) Name() string { return "setvariable" }
func (a ActorSetVariable) Validate(attrs *plugins.FieldCollection) (err error) {
func (a ActorSetVariable) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.String("variable"); err != nil || v == "" {
return errors.New("variable name must be non-empty string")
}
for _, field := range []string{"set", "variable"} {
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
return errors.Wrapf(err, "validating %s template", field)
}
}
return nil
}

View File

@ -13,6 +13,8 @@ const actorName = "whisper"
var (
botTwitchClient *twitch.Client
formatMessage plugins.MsgFormatter
ptrStringEmpty = func(s string) *string { return &s }("")
)
func Register(args plugins.RegistrationArguments) error {
@ -73,7 +75,7 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.String("to"); err != nil || v == "" {
return errors.New("to must be non-empty string")
}
@ -82,5 +84,11 @@ func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
return errors.New("message must be non-empty string")
}
for _, field := range []string{"message", "to"} {
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
return errors.Wrapf(err, "validating %s template", field)
}
}
return nil
}

View File

@ -13,8 +13,6 @@ import (
type actor struct{}
func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
ptrStringEmpty := func(v string) *string { return &v }("")
fd, err := formatMessage(attrs.MustString("fields", ptrStringEmpty), m, r, eventData)
if err != nil {
return false, errors.Wrap(err, "executing fields template")
@ -51,10 +49,16 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.String("fields"); err != nil || v == "" {
return errors.New("fields is expected to be non-empty string")
}
for _, field := range []string{"fields", "schedule_in"} {
if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil {
return errors.Wrapf(err, "validating %s template", field)
}
}
return nil
}

View File

@ -21,6 +21,8 @@ var (
eventCreatorFunc plugins.EventHandlerFunc
formatMessage plugins.MsgFormatter
mc *memoryCache
ptrStringEmpty = func(s string) *string { return &s }("")
)
func Register(args plugins.RegistrationArguments) error {

View File

@ -179,6 +179,7 @@ func handleSubCommand(args []string) {
fmt.Println(" actor-docs Generate markdown documentation for available actors")
fmt.Println(" api-token <name> <scope...> Generate an api-token to be entered into the config")
fmt.Println(" migrate-v2 <old file> Migrate old (*.json.gz) storage file into new database")
fmt.Println(" validate-config Try to load configuration file and report errors if any")
fmt.Println(" help Prints this help message")
case "migrate-v2":
@ -197,6 +198,11 @@ func handleSubCommand(args []string) {
log.Info("v2 storage file was migrated")
case "validate-config":
if err := loadConfig(cfg.Config); err != nil {
log.WithError(err).Fatal("loading config")
}
default:
handleSubCommand([]string{"help"})
log.Fatalf("Unknown sub-command %q", args[0])

View File

@ -86,3 +86,14 @@ func formatMessageFieldUserID(compiledFields *plugins.FieldCollection, m *irc.Me
func formatMessageFieldUsername(compiledFields *plugins.FieldCollection, m *irc.Message, fields *plugins.FieldCollection) {
compiledFields.Set("username", plugins.DeriveUser(m, fields))
}
func validateTemplate(tplString string) error {
// Template in frontend supports newlines, messages do not
tplString = stripNewline.ReplaceAllString(tplString, " ")
_, err := template.
New(tplString).
Funcs(tplFuncs.GetFuncMap(nil, nil, plugins.NewFieldCollection())).
Parse(tplString)
return errors.Wrap(err, "parsing template")
}

View File

@ -24,7 +24,7 @@ type (
// Validate will be called to validate the loaded configuration. It should
// return an error if required keys are missing from the AttributeStore
// or if keys contain broken configs
Validate(*FieldCollection) error
Validate(TemplateValidatorFunc, *FieldCollection) error
}
ActorCreationFunc func() Actor
@ -105,6 +105,8 @@ type (
TemplateFuncGetter func(*irc.Message, *Rule, *FieldCollection) interface{}
TemplateFuncRegister func(name string, fg TemplateFuncGetter)
TemplateValidatorFunc func(raw string) error
ValidateTokenFunc func(token string, modules ...string) error
)

View File

@ -193,6 +193,22 @@ func (r *Rule) UpdateFromSubscription() (bool, error) {
return true, nil
}
func (r Rule) Validate(tplValidate TemplateValidatorFunc) error {
if r.MatchMessage != nil {
if _, err := regexp.Compile(*r.MatchMessage); err != nil {
return errors.Wrap(err, "compiling match_message field regex")
}
}
if r.DisableOnTemplate != nil {
if err := tplValidate(*r.DisableOnTemplate); err != nil {
return errors.Wrap(err, "parsing disable_on_template template")
}
}
return nil
}
func (r *Rule) allowExecuteBadgeBlacklist(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool {
for _, b := range r.DisableOn {
if badges.Has(b) {