executor.go 7.4 KB

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