浏览代码

Exec feature 2k (#670)

James Read 8 月之前
父节点
当前提交
63c0142b6c

+ 1 - 0
service/internal/config/config.go

@@ -11,6 +11,7 @@ type Action struct {
 	Title                  string
 	Title                  string
 	Icon                   string
 	Icon                   string
 	Shell                  string
 	Shell                  string
+	Exec                   []string
 	ShellAfterCompleted    string
 	ShellAfterCompleted    string
 	Timeout                int
 	Timeout                int
 	Acls                   []string
 	Acls                   []string

+ 65 - 0
service/internal/executor/arguments.go

@@ -294,3 +294,68 @@ func mangleInvalidDatetimeValues(req *ExecutionRequest, arg *config.ActionArgume
 		req.Arguments[arg.Name] = timestamp.Format("2006-01-02T15:04:05")
 		req.Arguments[arg.Name] = timestamp.Format("2006-01-02T15:04:05")
 	}
 	}
 }
 }
+
+func parseActionExec(values map[string]string, action *config.Action, entityPrefix string) ([]string, error) {
+	for _, arg := range action.Arguments {
+		argName := arg.Name
+		argValue := values[argName]
+
+		err := typecheckActionArgument(&arg, argValue, action)
+
+		if err != nil {
+			return nil, err
+		}
+
+		log.WithFields(log.Fields{
+			"name":  argName,
+			"value": argValue,
+		}).Debugf("Arg assigned")
+	}
+
+	parsedArgs := make([]string, len(action.Exec))
+	for i, arg := range action.Exec {
+		parsedArg, err := parseCommandForReplacements(arg, values)
+		if err != nil {
+			return nil, err
+		}
+
+		parsedArg = sv.ReplaceEntityVars(entityPrefix, parsedArg)
+		parsedArgs[i] = parsedArg
+	}
+
+	redactedArgs := redactExecArgs(parsedArgs, action.Arguments, values)
+
+	log.WithFields(log.Fields{
+		"actionTitle": action.Title,
+		"cmd":         redactedArgs,
+	}).Infof("Action parse args - After (Exec)")
+
+	return parsedArgs, nil
+}
+
+//gocyclo:ignore
+func redactExecArgs(execArgs []string, arguments []config.ActionArgument, argumentValues map[string]string) []string {
+	redacted := make([]string, len(execArgs))
+	for i, arg := range execArgs {
+		redacted[i] = redactShellCommand(arg, arguments, argumentValues)
+	}
+	return redacted
+}
+
+func checkShellArgumentSafety(action *config.Action) error {
+	if action.Shell == "" {
+		return nil
+	}
+
+	unsafeTypes := []string{"url", "email", "raw_string_multiline", "very_dangerous_raw_string"}
+
+	for _, arg := range action.Arguments {
+		for _, unsafeType := range unsafeTypes {
+			if arg.Type == unsafeType {
+				return fmt.Errorf("unsafe argument type '%s' cannot be used with Shell execution. Use 'exec' instead. See https://docs.olivetin.app/action_execution/shellvsexec.html", arg.Type)
+			}
+		}
+	}
+
+	return nil
+}

+ 25 - 2
service/internal/executor/executor.go

@@ -15,6 +15,7 @@ import (
 	"context"
 	"context"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
+	"os/exec"
 	"path"
 	"path"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
@@ -67,6 +68,8 @@ type ExecutionRequest struct {
 
 
 	logEntry           *InternalLogEntry
 	logEntry           *InternalLogEntry
 	finalParsedCommand string
 	finalParsedCommand string
+	execArgs           []string
+	useDirectExec      bool
 	executor           *Executor
 	executor           *Executor
 }
 }
 
 
@@ -405,7 +408,21 @@ func stepParseArgs(req *ExecutionRequest) bool {
 
 
 	mangleInvalidArgumentValues(req)
 	mangleInvalidArgumentValues(req)
 
 
-	req.finalParsedCommand, err = parseActionArguments(req.Arguments, req.Action, req.EntityPrefix)
+	if len(req.Action.Exec) > 0 {
+		req.useDirectExec = true
+		req.execArgs, err = parseActionExec(req.Arguments, req.Action, req.EntityPrefix)
+	} else {
+		req.useDirectExec = false
+
+		err = checkShellArgumentSafety(req.Action)
+		if err != nil {
+			req.logEntry.Output = err.Error()
+			log.Warn(err.Error())
+			return false
+		}
+
+		req.finalParsedCommand, err = parseActionArguments(req.Arguments, req.Action, req.EntityPrefix)
+	}
 
 
 	if err != nil {
 	if err != nil {
 		req.logEntry.Output = err.Error()
 		req.logEntry.Output = err.Error()
@@ -545,7 +562,13 @@ func stepExec(req *ExecutionRequest) bool {
 
 
 	streamer := &OutputStreamer{Req: req}
 	streamer := &OutputStreamer{Req: req}
 
 
-	cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
+	var cmd *exec.Cmd
+	if req.useDirectExec {
+		cmd = wrapCommandDirect(ctx, req.execArgs)
+	} else {
+		cmd = wrapCommandInShell(ctx, req.finalParsedCommand)
+	}
+
 	cmd.Stdout = streamer
 	cmd.Stdout = streamer
 	cmd.Stderr = streamer
 	cmd.Stderr = streamer
 	cmd.Env = buildEnv(req.Arguments)
 	cmd.Env = buildEnv(req.Arguments)

+ 12 - 0
service/internal/executor/executor_unix.go

@@ -21,5 +21,17 @@ func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cm
 	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
 	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
 
 
 	return cmd
 	return cmd
+}
+
+func wrapCommandDirect(ctx context.Context, execArgs []string) *exec.Cmd {
+	if len(execArgs) == 0 {
+		return nil
+	}
 
 
+	cmd := exec.CommandContext(ctx, execArgs[0], execArgs[1:]...)
+
+	// This is to ensure that the process group is killed when the parent process is killed.
+	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
+
+	return cmd
 }
 }

+ 8 - 0
service/internal/executor/executor_windows.go

@@ -22,3 +22,11 @@ func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cm
 		return exec.CommandContext(ctx, "cmd", "/u", "/C", finalParsedCommand)
 		return exec.CommandContext(ctx, "cmd", "/u", "/C", finalParsedCommand)
 	}
 	}
 }
 }
+
+func wrapCommandDirect(ctx context.Context, execArgs []string) *exec.Cmd {
+	if len(execArgs) == 0 {
+		return nil
+	}
+
+	return exec.CommandContext(ctx, execArgs[0], execArgs[1:]...)
+}