diff --git a/api/projects/tasks.go b/api/projects/tasks.go index 5be12f2fe3..edfa97b8d3 100644 --- a/api/projects/tasks.go +++ b/api/projects/tasks.go @@ -77,6 +77,11 @@ func (c *TaskController) AddTask(w http.ResponseWriter, r *http.Request) { return } + if err := tpl.ValidateTaskMessage(taskObj.Message); err != nil { + helpers.WriteError(w, err) + return + } + newTask, err := taskPool(r).AddTaskFrom( r.Context(), audit.TriggerAPI, diff --git a/db/Template.go b/db/Template.go index 23fcc4086c..51b6e6a79d 100644 --- a/db/Template.go +++ b/db/Template.go @@ -366,6 +366,55 @@ type TemplateWithPerms struct { Permissions *ProjectUserPermission `db:"permissions" json:"permissions"` } +// TemplateMessageMode is what the task dialog does with the Message field for +// tasks launched from the template. +type TemplateMessageMode string + +const ( + TemplateMessageOptional TemplateMessageMode = "optional" + TemplateMessageRequired TemplateMessageMode = "required" + TemplateMessageHidden TemplateMessageMode = "hidden" +) + +// MessageMode reads task_params.message_mode. An absent, nil or empty value is +// TemplateMessageOptional; a value that is not one of the three modes is a +// validation error. +func (tpl *Template) MessageMode() (TemplateMessageMode, error) { + raw, ok := tpl.TaskParams["message_mode"] + if !ok || raw == nil { + return TemplateMessageOptional, nil + } + + s, ok := raw.(string) + if !ok { + return "", common_errors.NewValidationError("invalid task params: message_mode must be a string") + } + + switch mode := TemplateMessageMode(s); mode { + case "", TemplateMessageOptional: + return TemplateMessageOptional, nil + case TemplateMessageRequired, TemplateMessageHidden: + return mode, nil + default: + return "", common_errors.NewValidationError(fmt.Sprintf("invalid task params: unknown message_mode %q", s)) + } +} + +// ValidateTaskMessage rejects a blank message when the template's message mode +// is TemplateMessageRequired. +func (tpl *Template) ValidateTaskMessage(message string) error { + mode, err := tpl.MessageMode() + if err != nil { + return err + } + + if mode == TemplateMessageRequired && strings.TrimSpace(message) == "" { + return common_errors.NewValidationError("template requires a task message") + } + + return nil +} + func (tpl *Template) FillParams(target any) error { content, err := json.Marshal(tpl.TaskParams) if err != nil { @@ -436,6 +485,10 @@ func (tpl *Template) Validate() error { } } + if _, err := tpl.MessageMode(); err != nil { + return err + } + if tpl.Name == "" { return common_errors.NewValidationError("template name can not be empty") } diff --git a/db/Template_test.go b/db/Template_test.go index 50152a72db..9d546df160 100644 --- a/db/Template_test.go +++ b/db/Template_test.go @@ -216,3 +216,67 @@ func TestTemplateValidate_GalaxyArgs(t *testing.T) { tpl = newTemplate(MapStringAnyField{"galaxy_collection_args": "--pre"}) assert.ErrorContains(t, tpl.Validate(), "invalid task params") } + +func TestTemplateValidate_MessageMode(t *testing.T) { + util.Config = &util.ConfigType{Apps: map[string]util.App{string(AppAnsible): {}}} + defer func() { util.Config = nil }() + + inventoryID := 1 + newTemplate := func(taskParams MapStringAnyField) Template { + return Template{ + Name: "test", + Playbook: "playbook.yml", + App: AppAnsible, + InventoryID: &inventoryID, + TaskParams: taskParams, + } + } + + for _, mode := range []string{"optional", "required", "hidden", ""} { + tpl := newTemplate(MapStringAnyField{"message_mode": mode}) + assert.NoError(t, tpl.Validate(), mode) + } + + tpl := newTemplate(nil) + assert.NoError(t, tpl.Validate()) + + tpl = newTemplate(MapStringAnyField{"message_mode": "mandatory"}) + assert.ErrorContains(t, tpl.Validate(), `unknown message_mode "mandatory"`) + + tpl = newTemplate(MapStringAnyField{"message_mode": true}) + assert.ErrorContains(t, tpl.Validate(), "message_mode must be a string") +} + +func TestTemplateMessageMode(t *testing.T) { + for _, tc := range []struct { + params MapStringAnyField + want TemplateMessageMode + }{ + {nil, TemplateMessageOptional}, + {MapStringAnyField{}, TemplateMessageOptional}, + {MapStringAnyField{"message_mode": nil}, TemplateMessageOptional}, + {MapStringAnyField{"message_mode": ""}, TemplateMessageOptional}, + {MapStringAnyField{"message_mode": "required"}, TemplateMessageRequired}, + {MapStringAnyField{"message_mode": "hidden"}, TemplateMessageHidden}, + } { + tpl := Template{TaskParams: tc.params} + mode, err := tpl.MessageMode() + assert.NoError(t, err) + assert.Equal(t, tc.want, mode) + } +} + +func TestTemplateValidateTaskMessage(t *testing.T) { + required := Template{TaskParams: MapStringAnyField{"message_mode": "required"}} + assert.NoError(t, required.ValidateTaskMessage("RITM123456")) + assert.ErrorContains(t, required.ValidateTaskMessage(""), "template requires a task message") + assert.ErrorContains(t, required.ValidateTaskMessage(" \t"), "template requires a task message") + + for _, params := range []MapStringAnyField{nil, {"message_mode": "optional"}, {"message_mode": "hidden"}} { + tpl := Template{TaskParams: params} + assert.NoError(t, tpl.ValidateTaskMessage("")) + } + + bad := Template{TaskParams: MapStringAnyField{"message_mode": "mandatory"}} + assert.Error(t, bad.ValidateTaskMessage("any")) +} diff --git a/web/src/components/TaskForm.vue b/web/src/components/TaskForm.vue index e4554237b7..f2ea012705 100644 --- a/web/src/components/TaskForm.vue +++ b/web/src/components/TaskForm.vue @@ -55,8 +55,10 @@ > !!(v && v.trim()) || `${this.$t('message')} ${this.$t('isRequired')}`]; + }, + needInventory() { return this.needField('inventory') && this.template.task_params?.allow_override_inventory; }, diff --git a/web/src/components/TaskParamsForm.vue b/web/src/components/TaskParamsForm.vue index 3ce56e91d0..61bc404e32 100644 --- a/web/src/components/TaskParamsForm.vue +++ b/web/src/components/TaskParamsForm.vue @@ -1,6 +1,13 @@