| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- package executor
- import (
- pb "github.com/OliveTin/OliveTin/gen/grpc"
- acl "github.com/OliveTin/OliveTin/internal/acl"
- config "github.com/OliveTin/OliveTin/internal/config"
- log "github.com/sirupsen/logrus"
- "bytes"
- "context"
- "errors"
- "os/exec"
- "net/url"
- "regexp"
- "strings"
- "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 \\,\\.]+$",
- }
- )
- // 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
- // easily serializable.
- type InternalLogEntry struct {
- Datetime string
- Stdout string
- Stderr string
- TimedOut bool
- ExitCode int32
- /*
- The following two properties are obviously on Action normally, but it's useful
- that logs are lightweight (so we don't need to have an action associated to
- logs, etc. Therefore, we duplicate those values here.
- */
- ActionTitle 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
- }
- // Executor represents a helper class for executing commands. It's main method
- // is ExecRequest
- type Executor struct {
- 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)
- }
- // ExecRequest processes an ExecutionRequest
- func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
- req.logEntry = &InternalLogEntry{
- Datetime: time.Now().Format("2006-01-02 15:04:05"),
- ActionTitle: req.ActionName,
- Stdout: "",
- Stderr: "",
- ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
- }
- for _, step := range e.chainOfCommand {
- if !step.Exec(req) {
- break
- }
- }
- e.Logs = append(e.Logs, *req.logEntry)
- return &pb.StartActionResponse{
- LogEntry: &pb.LogEntry{
- ActionTitle: req.logEntry.ActionTitle,
- ActionIcon: req.logEntry.ActionIcon,
- Datetime: req.logEntry.Datetime,
- Stderr: req.logEntry.Stderr,
- Stdout: req.logEntry.Stdout,
- TimedOut: req.logEntry.TimedOut,
- ExitCode: req.logEntry.ExitCode,
- },
- }
- }
- 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")
- return true
- }
- type stepLogFinish struct{}
- 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")
- return true
- }
- type stepParseArgs struct{}
- func (e stepParseArgs) Exec(req *ExecutionRequest) bool {
- var err error
- req.finalParsedCommand, err = parseActionArguments(req.action.Shell, req.Arguments, req.action)
- if err != nil {
- req.logEntry.Stdout = err.Error()
- log.Warnf(err.Error())
- return false
- }
- return true
- }
- type stepExec struct{}
- func (e stepExec) Exec(req *ExecutionRequest) bool {
- ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.action.Timeout)*time.Second)
- defer cancel()
- var stdout bytes.Buffer
- var stderr bytes.Buffer
- cmd := exec.CommandContext(ctx, "sh", "-c", req.finalParsedCommand)
- cmd.Stdout = &stdout
- cmd.Stderr = &stderr
- runerr := cmd.Run()
- req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
- req.logEntry.Stdout = stdout.String()
- req.logEntry.Stderr = stderr.String()
- if runerr != nil {
- req.logEntry.Stderr = runerr.Error() + "\n\n" + req.logEntry.Stderr
- }
- if ctx.Err() == context.DeadlineExceeded {
- req.logEntry.TimedOut = 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
- }
|