executor.go 7.0 KB

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