|
@@ -8,23 +8,21 @@ import (
|
|
|
|
|
|
|
|
"bytes"
|
|
"bytes"
|
|
|
"context"
|
|
"context"
|
|
|
- "errors"
|
|
|
|
|
"os/exec"
|
|
"os/exec"
|
|
|
- "net/url"
|
|
|
|
|
- "regexp"
|
|
|
|
|
- "strings"
|
|
|
|
|
"time"
|
|
"time"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
-var (
|
|
|
|
|
- typecheckRegex = map[string]string{
|
|
|
|
|
- "very_dangerous_raw_string": "",
|
|
|
|
|
- "int": "^[\\d]+$",
|
|
|
|
|
- "ascii": "^[a-zA-Z0-9]+$",
|
|
|
|
|
- "ascii_identifier": "^[a-zA-Z0-9\\-\\.\\_]+$",
|
|
|
|
|
- "ascii_sentence": "^[a-zA-Z0-9 \\,\\.]+$",
|
|
|
|
|
- }
|
|
|
|
|
-)
|
|
|
|
|
|
|
+// ExecutionRequest is a request to execute an action. It's passed to an
|
|
|
|
|
+// Executor. They're created from the grpcapi.
|
|
|
|
|
+type ExecutionRequest struct {
|
|
|
|
|
+ ActionName string
|
|
|
|
|
+ Arguments map[string]string
|
|
|
|
|
+ action *config.Action
|
|
|
|
|
+ Cfg *config.Config
|
|
|
|
|
+ AuthenticatedUser *acl.AuthenticatedUser
|
|
|
|
|
+ logEntry *InternalLogEntry
|
|
|
|
|
+ finalParsedCommand string
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
// InternalLogEntry objects are created by an Executor, and represent the final
|
|
// InternalLogEntry objects are created by an Executor, and represent the final
|
|
|
// state of execution (even if the command is not executed). It's designed to be
|
|
// state of execution (even if the command is not executed). It's designed to be
|
|
@@ -45,71 +43,14 @@ type InternalLogEntry struct {
|
|
|
ActionIcon string
|
|
ActionIcon string
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// ExecutionRequest is a request to execute an action. It's passed to an
|
|
|
|
|
-// Executor. They're created from the grpcapi.
|
|
|
|
|
-type ExecutionRequest struct {
|
|
|
|
|
- ActionName string
|
|
|
|
|
- Arguments map[string]string
|
|
|
|
|
- action *config.Action
|
|
|
|
|
- Cfg *config.Config
|
|
|
|
|
- AuthenticatedUser *acl.AuthenticatedUser
|
|
|
|
|
- logEntry *InternalLogEntry
|
|
|
|
|
- finalParsedCommand string
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-type executorStep interface {
|
|
|
|
|
- Exec(*ExecutionRequest) bool
|
|
|
|
|
-}
|
|
|
|
|
|
|
+type executorStepFunc func(*ExecutionRequest) bool
|
|
|
|
|
|
|
|
// Executor represents a helper class for executing commands. It's main method
|
|
// Executor represents a helper class for executing commands. It's main method
|
|
|
// is ExecRequest
|
|
// is ExecRequest
|
|
|
type Executor struct {
|
|
type Executor struct {
|
|
|
Logs []InternalLogEntry
|
|
Logs []InternalLogEntry
|
|
|
|
|
|
|
|
- chainOfCommand []executorStep
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// DefaultExecutor returns an Executor, with a sensible "chain of command" for
|
|
|
|
|
-// executing actions.
|
|
|
|
|
-func DefaultExecutor() *Executor {
|
|
|
|
|
- e := Executor{}
|
|
|
|
|
- e.chainOfCommand = []executorStep{
|
|
|
|
|
- stepFindAction{},
|
|
|
|
|
- stepACLCheck{},
|
|
|
|
|
- stepParseArgs{},
|
|
|
|
|
- stepLogStart{},
|
|
|
|
|
- stepExec{},
|
|
|
|
|
- stepLogFinish{},
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return &e
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-type stepFindAction struct{}
|
|
|
|
|
-
|
|
|
|
|
-func (s stepFindAction) Exec(req *ExecutionRequest) bool {
|
|
|
|
|
- actualAction := req.Cfg.FindAction(req.ActionName)
|
|
|
|
|
-
|
|
|
|
|
- if actualAction == nil {
|
|
|
|
|
- log.WithFields(log.Fields{
|
|
|
|
|
- "actionName": req.ActionName,
|
|
|
|
|
- }).Warnf("Action not found")
|
|
|
|
|
-
|
|
|
|
|
- req.logEntry.Stderr = "Action not found"
|
|
|
|
|
-
|
|
|
|
|
- return false
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- req.action = actualAction
|
|
|
|
|
- req.logEntry.ActionIcon = actualAction.Icon
|
|
|
|
|
-
|
|
|
|
|
- return true
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-type stepACLCheck struct{}
|
|
|
|
|
-
|
|
|
|
|
-func (s stepACLCheck) Exec(req *ExecutionRequest) bool {
|
|
|
|
|
- return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.action)
|
|
|
|
|
|
|
+ chainOfCommand []executorStepFunc
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ExecRequest processes an ExecutionRequest
|
|
// ExecRequest processes an ExecutionRequest
|
|
@@ -123,7 +64,7 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
for _, step := range e.chainOfCommand {
|
|
for _, step := range e.chainOfCommand {
|
|
|
- if !step.Exec(req) {
|
|
|
|
|
|
|
+ if !step(req) {
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -143,34 +84,46 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-type stepLogStart struct{}
|
|
|
|
|
-
|
|
|
|
|
-func (e stepLogStart) Exec(req *ExecutionRequest) bool {
|
|
|
|
|
- log.WithFields(log.Fields{
|
|
|
|
|
- "title": req.action.Title,
|
|
|
|
|
- "timeout": req.action.Timeout,
|
|
|
|
|
- }).Infof("Action starting")
|
|
|
|
|
|
|
+// DefaultExecutor returns an Executor, with a sensible "chain of command" for
|
|
|
|
|
+// executing actions.
|
|
|
|
|
+func DefaultExecutor() *Executor {
|
|
|
|
|
+ e := Executor{}
|
|
|
|
|
+ e.chainOfCommand = []executorStepFunc{
|
|
|
|
|
+ stepFindAction,
|
|
|
|
|
+ stepACLCheck,
|
|
|
|
|
+ stepParseArgs,
|
|
|
|
|
+ stepLogStart,
|
|
|
|
|
+ stepExec,
|
|
|
|
|
+ stepLogFinish,
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- return true
|
|
|
|
|
|
|
+ return &e
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-type stepLogFinish struct{}
|
|
|
|
|
|
|
+func stepFindAction(req *ExecutionRequest) bool {
|
|
|
|
|
+ actualAction := req.Cfg.FindAction(req.ActionName)
|
|
|
|
|
|
|
|
-func (e stepLogFinish) Exec(req *ExecutionRequest) bool {
|
|
|
|
|
- log.WithFields(log.Fields{
|
|
|
|
|
- "title": req.action.Title,
|
|
|
|
|
- "stdout": req.logEntry.Stdout,
|
|
|
|
|
- "stderr": req.logEntry.Stderr,
|
|
|
|
|
- "timedOut": req.logEntry.TimedOut,
|
|
|
|
|
- "exit": req.logEntry.ExitCode,
|
|
|
|
|
- }).Infof("Action finished")
|
|
|
|
|
|
|
+ if actualAction == nil {
|
|
|
|
|
+ log.WithFields(log.Fields{
|
|
|
|
|
+ "actionName": req.ActionName,
|
|
|
|
|
+ }).Warnf("Action not found")
|
|
|
|
|
+
|
|
|
|
|
+ req.logEntry.Stderr = "Action not found"
|
|
|
|
|
+
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ req.action = actualAction
|
|
|
|
|
+ req.logEntry.ActionIcon = actualAction.Icon
|
|
|
|
|
|
|
|
return true
|
|
return true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-type stepParseArgs struct{}
|
|
|
|
|
|
|
+func stepACLCheck(req *ExecutionRequest) bool {
|
|
|
|
|
+ return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.action)
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-func (e stepParseArgs) Exec(req *ExecutionRequest) bool {
|
|
|
|
|
|
|
+func stepParseArgs(req *ExecutionRequest) bool {
|
|
|
var err error
|
|
var err error
|
|
|
|
|
|
|
|
req.finalParsedCommand, err = parseActionArguments(req.action.Shell, req.Arguments, req.action)
|
|
req.finalParsedCommand, err = parseActionArguments(req.action.Shell, req.Arguments, req.action)
|
|
@@ -186,9 +139,28 @@ func (e stepParseArgs) Exec(req *ExecutionRequest) bool {
|
|
|
return true
|
|
return true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-type stepExec struct{}
|
|
|
|
|
|
|
+func stepLogStart(req *ExecutionRequest) bool {
|
|
|
|
|
+ log.WithFields(log.Fields{
|
|
|
|
|
+ "title": req.action.Title,
|
|
|
|
|
+ "timeout": req.action.Timeout,
|
|
|
|
|
+ }).Infof("Action starting")
|
|
|
|
|
|
|
|
-func (e stepExec) Exec(req *ExecutionRequest) bool {
|
|
|
|
|
|
|
+ return true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func stepLogFinish(req *ExecutionRequest) bool {
|
|
|
|
|
+ log.WithFields(log.Fields{
|
|
|
|
|
+ "title": req.action.Title,
|
|
|
|
|
+ "stdout": req.logEntry.Stdout,
|
|
|
|
|
+ "stderr": req.logEntry.Stderr,
|
|
|
|
|
+ "timedOut": req.logEntry.TimedOut,
|
|
|
|
|
+ "exit": req.logEntry.ExitCode,
|
|
|
|
|
+ }).Infof("Action finished")
|
|
|
|
|
+
|
|
|
|
|
+ return true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func stepExec(req *ExecutionRequest) bool {
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.action.Timeout)*time.Second)
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.action.Timeout)*time.Second)
|
|
|
defer cancel()
|
|
defer cancel()
|
|
|
|
|
|
|
@@ -215,103 +187,3 @@ func (e stepExec) Exec(req *ExecutionRequest) bool {
|
|
|
|
|
|
|
|
return true
|
|
return true
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-func parseActionArguments(rawShellCommand string, values map[string]string, action *config.Action) (string, error) {
|
|
|
|
|
- log.WithFields(log.Fields{
|
|
|
|
|
- "cmd": rawShellCommand,
|
|
|
|
|
- }).Infof("Before Parse Args")
|
|
|
|
|
-
|
|
|
|
|
- r := regexp.MustCompile("{{ *?([a-zA-Z0-9_]+?) *?}}")
|
|
|
|
|
- matches := r.FindAllStringSubmatch(rawShellCommand, -1)
|
|
|
|
|
-
|
|
|
|
|
- for _, match := range matches {
|
|
|
|
|
- argValue, argProvided := values[match[1]]
|
|
|
|
|
-
|
|
|
|
|
- if !argProvided {
|
|
|
|
|
- log.Infof("%v", values)
|
|
|
|
|
- return "", errors.New("Required arg not provided: " + match[1])
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- err := typecheckActionArgument(match[1], argValue, action)
|
|
|
|
|
-
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return "", err
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- log.WithFields(log.Fields{
|
|
|
|
|
- "name": match[1],
|
|
|
|
|
- "value": argValue,
|
|
|
|
|
- }).Debugf("Arg assigned")
|
|
|
|
|
-
|
|
|
|
|
- rawShellCommand = strings.ReplaceAll(rawShellCommand, match[0], argValue)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- log.WithFields(log.Fields{
|
|
|
|
|
- "cmd": rawShellCommand,
|
|
|
|
|
- }).Infof("After Parse Args")
|
|
|
|
|
-
|
|
|
|
|
- return rawShellCommand, nil
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-func typecheckActionArgument(name string, value string, action *config.Action) error {
|
|
|
|
|
- arg := action.FindArg(name)
|
|
|
|
|
-
|
|
|
|
|
- if arg == nil {
|
|
|
|
|
- return errors.New("Action arg not defined: " + name)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if len(arg.Choices) > 0 {
|
|
|
|
|
- return typecheckChoice(value, arg)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return TypeSafetyCheck(name, value, arg.Type)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-func typecheckChoice(value string, arg *config.ActionArgument) error {
|
|
|
|
|
- for _, choice := range arg.Choices {
|
|
|
|
|
- if value == choice.Value {
|
|
|
|
|
- return nil
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return errors.New("argument value is not one of the predefined choices")
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// TypeSafetyCheck checks argument values match a specific type. The types are
|
|
|
|
|
-// defined in typecheckRegex, and, you guessed it, uses regex to check for allowed
|
|
|
|
|
-// characters.
|
|
|
|
|
-func TypeSafetyCheck(name string, value string, argumentType string) error {
|
|
|
|
|
- if argumentType == "url" {
|
|
|
|
|
- return typeSafetyCheckUrl(name, value)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return typeSafetyCheckRegex(name, value, argumentType)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-func typeSafetyCheckRegex(name string, value string, argumentType string) error {
|
|
|
|
|
- pattern, found := typecheckRegex[argumentType]
|
|
|
|
|
-
|
|
|
|
|
- if !found {
|
|
|
|
|
- return errors.New("argument type not implemented " + argumentType)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- matches, _ := regexp.MatchString(pattern, value)
|
|
|
|
|
-
|
|
|
|
|
- if !matches {
|
|
|
|
|
- log.WithFields(log.Fields{
|
|
|
|
|
- "name": name,
|
|
|
|
|
- "value": value,
|
|
|
|
|
- "type": argumentType,
|
|
|
|
|
- }).Warn("Arg type check safety failure")
|
|
|
|
|
-
|
|
|
|
|
- return errors.New("invalid argument, doesn't match " + argumentType)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return nil
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-func typeSafetyCheckUrl(name string, value string) error {
|
|
|
|
|
- _, err := url.ParseRequestURI(value)
|
|
|
|
|
-
|
|
|
|
|
- return err
|
|
|
|
|
-}
|
|
|