sanitize.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. package config
  2. import (
  3. "fmt"
  4. "strings"
  5. "text/template"
  6. "github.com/OliveTin/OliveTin/internal/env"
  7. "github.com/google/uuid"
  8. log "github.com/sirupsen/logrus"
  9. )
  10. // Sanitize will look for common configuration issues, and fix them. For example,
  11. // populating undefined fields - name -> title, etc.
  12. func (cfg *Config) Sanitize() {
  13. cfg.sanitizeLogLevel()
  14. cfg.sanitizeAuthRequireGuestsToLogin()
  15. cfg.sanitizeLogHistoryPageSize()
  16. cfg.sanitizeLocalUsers()
  17. cfg.sanitizeSecurityHeaders()
  18. // log.Infof("cfg %p", cfg)
  19. for idx := range cfg.Actions {
  20. cfg.Actions[idx].sanitize(cfg)
  21. }
  22. cfg.sanitizeDashboardsForInlineActions()
  23. if err := cfg.validateReservedActionArgumentNames(); err != nil {
  24. log.Fatalf("%v", err)
  25. }
  26. }
  27. func (cfg *Config) validateReservedActionArgumentNames() error {
  28. for _, action := range cfg.Actions {
  29. if err := action.validateReservedArgumentNames(); err != nil {
  30. return err
  31. }
  32. }
  33. return nil
  34. }
  35. func (action *Action) validateReservedArgumentNames() error {
  36. if action == nil {
  37. return nil
  38. }
  39. for _, arg := range action.Arguments {
  40. if strings.HasPrefix(arg.Name, ReservedArgumentNamePrefix) {
  41. return fmt.Errorf("action %q argument %q uses reserved prefix %q", action.Title, arg.Name, ReservedArgumentNamePrefix)
  42. }
  43. }
  44. return nil
  45. }
  46. func (cfg *Config) sanitizeDashboardsForInlineActions() {
  47. for _, dashboard := range cfg.Dashboards {
  48. cfg.sanitizeDashboardComponentForInlineActions(dashboard)
  49. }
  50. }
  51. func (cfg *Config) sanitizeDashboardComponentForInlineActions(component *DashboardComponent) {
  52. visited := make(map[*DashboardComponent]bool)
  53. cfg.sanitizeDashboardComponentForInlineActionsHelper(component, visited)
  54. }
  55. func (cfg *Config) sanitizeDashboardComponentForInlineActionsHelper(component *DashboardComponent, visited map[*DashboardComponent]bool) {
  56. if component == nil {
  57. return
  58. }
  59. if visited[component] {
  60. return
  61. }
  62. visited[component] = true
  63. cfg.sanitizeInlineAction(component)
  64. cfg.sanitizeChildDashboardComponents(component, visited)
  65. }
  66. func (cfg *Config) sanitizeInlineAction(component *DashboardComponent) {
  67. if component.InlineAction == nil {
  68. return
  69. }
  70. sanitizeInlineActionTitles(component)
  71. if component.Entity != "" && component.InlineAction.Entity == "" {
  72. component.InlineAction.Entity = component.Entity
  73. }
  74. component.InlineAction.sanitize(cfg)
  75. cfg.addInlineActionIfNotExists(component.InlineAction)
  76. }
  77. func (cfg *Config) addInlineActionIfNotExists(action *Action) {
  78. if cfg.inlineActionExists(action) {
  79. return
  80. }
  81. cfg.Actions = append(cfg.Actions, action)
  82. }
  83. func sanitizeInlineActionTitles(component *DashboardComponent) {
  84. if component.InlineAction.Title == "" {
  85. component.InlineAction.Title = component.Title
  86. }
  87. if component.Title == "" {
  88. component.Title = component.InlineAction.Title
  89. }
  90. }
  91. func (cfg *Config) inlineActionExists(action *Action) bool {
  92. if cfg.inlineActionPointerExists(action) {
  93. return true
  94. }
  95. if cfg.inlineActionIDExists(action) {
  96. return true
  97. }
  98. return false
  99. }
  100. func (cfg *Config) inlineActionPointerExists(action *Action) bool {
  101. for _, existingAction := range cfg.Actions {
  102. if existingAction == action {
  103. return true
  104. }
  105. }
  106. return false
  107. }
  108. func (cfg *Config) inlineActionIDExists(action *Action) bool {
  109. if action.ID == "" {
  110. return false
  111. }
  112. for _, existingAction := range cfg.Actions {
  113. if existingAction.ID == action.ID {
  114. return true
  115. }
  116. }
  117. return false
  118. }
  119. func (cfg *Config) sanitizeChildDashboardComponents(component *DashboardComponent, visited map[*DashboardComponent]bool) {
  120. for _, child := range component.Contents {
  121. if child.Entity == "" {
  122. child.Entity = component.Entity
  123. }
  124. cfg.sanitizeDashboardComponentForInlineActionsHelper(child, visited)
  125. }
  126. }
  127. func (cfg *Config) sanitizeLogLevel() {
  128. if logLevel, err := log.ParseLevel(cfg.LogLevel); err == nil {
  129. log.Info("Setting log level to ", logLevel)
  130. log.SetLevel(logLevel)
  131. }
  132. }
  133. func (action *Action) sanitize(cfg *Config) {
  134. if action.Timeout < 3 {
  135. action.Timeout = 3
  136. }
  137. action.ID = getActionID(action)
  138. action.Icon = lookupHTMLIcon(action.Icon, cfg.DefaultIconForActions)
  139. action.PopupOnStart = sanitizePopupOnStart(action.PopupOnStart, cfg)
  140. if action.MaxConcurrent < 1 {
  141. action.MaxConcurrent = 1
  142. }
  143. for idx := range action.Arguments {
  144. action.Arguments[idx].sanitize()
  145. }
  146. }
  147. func (cfg *Config) sanitizeAuthRequireGuestsToLogin() {
  148. if cfg.AuthRequireGuestsToLogin {
  149. log.Infof("AuthRequireGuestsToLogin is enabled. All defaultPermissions will be set to false")
  150. cfg.DefaultPermissions.View = false
  151. cfg.DefaultPermissions.Exec = false
  152. cfg.DefaultPermissions.Logs = false
  153. cfg.DefaultPermissions.Kill = false
  154. }
  155. }
  156. func (cfg *Config) sanitizeLogHistoryPageSize() {
  157. if cfg.LogHistoryPageSize < 10 {
  158. log.Warnf("LogsHistoryLimit is too low, setting it to 10")
  159. cfg.LogHistoryPageSize = 10
  160. } else if cfg.LogHistoryPageSize > 100 {
  161. log.Warnf("LogsHistoryLimit is high, you can do this, but expect browser lag.")
  162. }
  163. }
  164. func (cfg *Config) sanitizeLocalUsers() {
  165. for _, user := range cfg.AuthLocalUsers.Users {
  166. expandLocalUserEnvTemplates(user)
  167. }
  168. if err := validateUniqueLocalUserAPIKeys(cfg.AuthLocalUsers.Users); err != nil {
  169. log.Fatalf("%v", err)
  170. }
  171. }
  172. func expandLocalUserEnvTemplates(user *LocalUser) {
  173. if user == nil {
  174. return
  175. }
  176. if user.Password != "" {
  177. user.Password = expandEnvTemplate(user.Password)
  178. }
  179. if user.ApiKey != "" {
  180. user.ApiKey = expandEnvTemplate(user.ApiKey)
  181. }
  182. }
  183. // validateUniqueLocalUserAPIKeys returns an error when two local users share the same non-empty apiKey.
  184. func validateUniqueLocalUserAPIKeys(users []*LocalUser) error {
  185. seen := make(map[string]string)
  186. for _, user := range users {
  187. if err := recordUniqueLocalUserAPIKey(seen, user); err != nil {
  188. return err
  189. }
  190. }
  191. return nil
  192. }
  193. func recordUniqueLocalUserAPIKey(seen map[string]string, user *LocalUser) error {
  194. if user == nil || user.ApiKey == "" {
  195. return nil
  196. }
  197. if prior, ok := seen[user.ApiKey]; ok {
  198. return fmt.Errorf("duplicate authLocalUsers apiKey for users %q and %q", prior, user.Username)
  199. }
  200. seen[user.ApiKey] = user.Username
  201. return nil
  202. }
  203. func (cfg *Config) sanitizeSecurityHeaders() {
  204. cfg.sanitizeSecurityHeadersCSP()
  205. cfg.sanitizeSecurityHeadersXFrameOptions()
  206. }
  207. func (cfg *Config) sanitizeSecurityHeadersCSP() {
  208. if !cfg.Security.HeaderContentSecurityPolicy || cfg.Security.ContentSecurityPolicy != "" {
  209. return
  210. }
  211. cfg.Security.ContentSecurityPolicy = ContentSecurityPolicyDefault
  212. }
  213. func (cfg *Config) sanitizeSecurityHeadersXFrameOptions() {
  214. if !cfg.Security.HeaderXFrameOptions || cfg.Security.XFrameOptions != "" {
  215. return
  216. }
  217. cfg.Security.XFrameOptions = "DENY"
  218. }
  219. // expandEnvTemplate expands {{ .Env.VAR }} in config strings using the process environment.
  220. func expandEnvTemplate(source string) string {
  221. t, err := template.New("envTemplate").Option("missingkey=error").Parse(source)
  222. if err != nil {
  223. log.WithFields(log.Fields{"error": err}).Debug("Env template parse failed, using literal")
  224. return source
  225. }
  226. var b strings.Builder
  227. if err := t.Execute(&b, map[string]interface{}{"Env": env.BuildEnvMap()}); err != nil {
  228. log.WithFields(log.Fields{"error": err}).Debug("Env template execute failed, using literal")
  229. return source
  230. }
  231. return b.String()
  232. }
  233. func getActionID(action *Action) string {
  234. if action.ID == "" {
  235. return uuid.NewString()
  236. }
  237. if strings.Contains(action.ID, "{{") {
  238. log.Fatalf("Action IDs cannot contain variables")
  239. }
  240. return action.ID
  241. }
  242. //gocyclo:ignore
  243. func sanitizePopupOnStart(raw string, cfg *Config) string {
  244. switch raw {
  245. case "execution-dialog":
  246. return raw
  247. case "execution-dialog-output-html":
  248. return raw
  249. case "execution-dialog-stdout-only":
  250. return raw
  251. case "execution-button":
  252. return raw
  253. case "history":
  254. return raw
  255. default:
  256. return cfg.DefaultPopupOnStart
  257. }
  258. }
  259. func (arg *ActionArgument) sanitize() {
  260. if arg.Title == "" {
  261. arg.Title = arg.Name
  262. }
  263. for idx, choice := range arg.Choices {
  264. if choice.Title == "" {
  265. arg.Choices[idx].Title = choice.Value
  266. }
  267. }
  268. arg.sanitizeNoType()
  269. // Default value validation runs in executor at config load (validateArgumentDefaults).
  270. }
  271. func (arg *ActionArgument) sanitizeNoType() {
  272. if len(arg.Choices) == 0 && arg.Type == "" {
  273. log.WithFields(log.Fields{
  274. "arg": arg.Name,
  275. }).Warn("Argument type isn't set, will default to 'ascii' but this may not be safe. You should set a type specifically.")
  276. arg.Type = "ascii"
  277. }
  278. }