4
0

executor.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. package executor
  2. import (
  3. pb "github.com/jamesread/OliveTin/gen/grpc"
  4. acl "github.com/jamesread/OliveTin/internal/acl"
  5. config "github.com/jamesread/OliveTin/internal/config"
  6. log "github.com/sirupsen/logrus"
  7. "context"
  8. "errors"
  9. "os/exec"
  10. "regexp"
  11. "strings"
  12. "time"
  13. )
  14. var (
  15. typecheckRegex = map[string]string{
  16. "very_dangerous_raw_string": "",
  17. "int": "^[\\d]+$",
  18. "ascii": "^[a-zA-Z0-9]+$",
  19. "ascii_identifier": "^[a-zA-Z0-9\\-\\.\\_]+$",
  20. "ascii_sentence": "^[a-zA-Z0-9 \\,\\.]+$",
  21. }
  22. )
  23. // InternalLogEntry objects are created by an Executor, and represent the final
  24. // state of execution (even if the command is not executed). It's designed to be
  25. // easily serializable.
  26. type InternalLogEntry struct {
  27. Datetime string
  28. Stdout string
  29. Stderr string
  30. TimedOut bool
  31. ExitCode int32
  32. /*
  33. The following two properties are obviously on Action normally, but it's useful
  34. that logs are lightweight (so we don't need to have an action associated to
  35. logs, etc. Therefore, we duplicate those values here.
  36. */
  37. ActionTitle string
  38. ActionIcon string
  39. }
  40. // ExecutionRequest is a request to execute an action. It's passed to an
  41. // Executor. They're created from the grpcapi.
  42. type ExecutionRequest struct {
  43. ActionName string
  44. Arguments map[string]string
  45. action *config.Action
  46. Cfg *config.Config
  47. User *acl.User
  48. logEntry *InternalLogEntry
  49. finalParsedCommand string
  50. }
  51. type executorStep interface {
  52. Exec(*ExecutionRequest) bool
  53. }
  54. // Executor represents a helper class for executing commands. It's main method
  55. // is ExecRequest
  56. type Executor struct {
  57. Logs []InternalLogEntry
  58. chainOfCommand []executorStep
  59. }
  60. // DefaultExecutor returns an Executor, with a sensible "chain of command" for
  61. // executing actions.
  62. func DefaultExecutor() *Executor {
  63. e := Executor{}
  64. e.chainOfCommand = []executorStep{
  65. stepFindAction{},
  66. stepACLCheck{},
  67. stepParseArgs{},
  68. stepLogStart{},
  69. stepExec{},
  70. stepLogFinish{},
  71. }
  72. return &e
  73. }
  74. type stepFindAction struct{}
  75. func (s stepFindAction) Exec(req *ExecutionRequest) bool {
  76. actualAction := req.Cfg.FindAction(req.ActionName)
  77. if actualAction == nil {
  78. log.WithFields(log.Fields{
  79. "actionName": req.ActionName,
  80. }).Warnf("Action not found")
  81. req.logEntry.Stderr = "Action not found"
  82. return false
  83. }
  84. req.action = actualAction
  85. req.logEntry.ActionIcon = actualAction.Icon
  86. return true
  87. }
  88. type stepACLCheck struct{}
  89. func (s stepACLCheck) Exec(req *ExecutionRequest) bool {
  90. return acl.IsAllowedExec(req.Cfg, req.User, req.action)
  91. }
  92. // ExecRequest processes an ExecutionRequest
  93. func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
  94. req.logEntry = &InternalLogEntry{
  95. Datetime: time.Now().Format("2006-01-02 15:04:05"),
  96. ActionTitle: req.ActionName,
  97. Stdout: "",
  98. Stderr: "",
  99. ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
  100. }
  101. for _, step := range e.chainOfCommand {
  102. if !step.Exec(req) {
  103. break
  104. }
  105. }
  106. e.Logs = append(e.Logs, *req.logEntry)
  107. return &pb.StartActionResponse{
  108. LogEntry: &pb.LogEntry{
  109. ActionTitle: req.logEntry.ActionTitle,
  110. ActionIcon: req.logEntry.ActionIcon,
  111. Datetime: req.logEntry.Datetime,
  112. Stderr: req.logEntry.Stderr,
  113. Stdout: req.logEntry.Stdout,
  114. TimedOut: req.logEntry.TimedOut,
  115. ExitCode: req.logEntry.ExitCode,
  116. },
  117. }
  118. }
  119. type stepLogStart struct{}
  120. func (e stepLogStart) Exec(req *ExecutionRequest) bool {
  121. log.WithFields(log.Fields{
  122. "title": req.action.Title,
  123. "timeout": req.action.Timeout,
  124. }).Infof("Action starting")
  125. return true
  126. }
  127. type stepLogFinish struct{}
  128. func (e stepLogFinish) Exec(req *ExecutionRequest) bool {
  129. log.WithFields(log.Fields{
  130. "title": req.action.Title,
  131. "stdout": req.logEntry.Stdout,
  132. "stderr": req.logEntry.Stderr,
  133. "timedOut": req.logEntry.TimedOut,
  134. "exit": req.logEntry.ExitCode,
  135. }).Infof("Action finished")
  136. return true
  137. }
  138. type stepParseArgs struct{}
  139. func (e stepParseArgs) Exec(req *ExecutionRequest) bool {
  140. var err error
  141. req.finalParsedCommand, err = parseActionArguments(req.action.Shell, req.Arguments, req.action)
  142. if err != nil {
  143. req.logEntry.Stdout = err.Error()
  144. log.Warnf(err.Error())
  145. return false
  146. }
  147. return true
  148. }
  149. type stepExec struct{}
  150. func (e stepExec) Exec(req *ExecutionRequest) bool {
  151. ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.action.Timeout)*time.Second)
  152. defer cancel()
  153. cmd := exec.CommandContext(ctx, "sh", "-c", req.finalParsedCommand)
  154. stdout, stderr := cmd.Output()
  155. if stderr != nil {
  156. req.logEntry.Stderr = stderr.Error()
  157. }
  158. if ctx.Err() == context.DeadlineExceeded {
  159. req.logEntry.TimedOut = true
  160. }
  161. req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
  162. req.logEntry.Stdout = string(stdout)
  163. return true
  164. }
  165. func parseActionArguments(rawShellCommand string, values map[string]string, action *config.Action) (string, error) {
  166. log.WithFields(log.Fields{
  167. "cmd": rawShellCommand,
  168. }).Infof("Before Parse Args")
  169. r := regexp.MustCompile("{{ *?([a-z]+?) *?}}")
  170. matches := r.FindAllStringSubmatch(rawShellCommand, -1)
  171. for _, match := range matches {
  172. argValue, argProvided := values[match[1]]
  173. if !argProvided {
  174. log.Infof("%v", values)
  175. return "", errors.New("Required arg not provided: " + match[1])
  176. }
  177. err := typecheckActionArgument(match[1], argValue, action)
  178. if err != nil {
  179. return "", err
  180. }
  181. log.WithFields(log.Fields{
  182. "name": match[1],
  183. "value": argValue,
  184. }).Debugf("Arg assigned")
  185. rawShellCommand = strings.ReplaceAll(rawShellCommand, match[0], argValue)
  186. }
  187. log.WithFields(log.Fields{
  188. "cmd": rawShellCommand,
  189. }).Infof("After Parse Args")
  190. return rawShellCommand, nil
  191. }
  192. func typecheckActionArgument(name string, value string, action *config.Action) error {
  193. arg := action.FindArg(name)
  194. if arg == nil {
  195. return errors.New("Action arg not defined: " + name)
  196. }
  197. if len(arg.Choices) > 0 {
  198. return typecheckChoice(value, arg)
  199. }
  200. return TypeSafetyCheck(name, value, arg.Type)
  201. }
  202. func typecheckChoice(value string, arg *config.ActionArgument) error {
  203. for _, choice := range arg.Choices {
  204. if value == choice.Value {
  205. return nil
  206. }
  207. }
  208. return errors.New("argument value is not one of the predefined choices")
  209. }
  210. // TypeSafetyCheck checks argument values match a specific type. The types are
  211. // defined in typecheckRegex, and, you guessed it, uses regex to check for allowed
  212. // characters.
  213. func TypeSafetyCheck(name string, value string, typ string) error {
  214. pattern, found := typecheckRegex[typ]
  215. if !found {
  216. return errors.New("argument type not implemented " + typ)
  217. }
  218. matches, _ := regexp.MatchString(pattern, value)
  219. if !matches {
  220. log.WithFields(log.Fields{
  221. "name": name,
  222. "type": typ,
  223. "value": value,
  224. }).Warn("Arg type check safety failure")
  225. return errors.New("invalid argument, doesn't match " + typ)
  226. }
  227. return nil
  228. }