package config import ( "strings" "text/template" "github.com/OliveTin/OliveTin/internal/env" "github.com/google/uuid" log "github.com/sirupsen/logrus" ) // Sanitize will look for common configuration issues, and fix them. For example, // populating undefined fields - name -> title, etc. func (cfg *Config) Sanitize() { cfg.sanitizeLogLevel() cfg.sanitizeAuthRequireGuestsToLogin() cfg.sanitizeLogHistoryPageSize() cfg.sanitizeLocalUserPasswords() // log.Infof("cfg %p", cfg) for idx := range cfg.Actions { cfg.Actions[idx].sanitize(cfg) } cfg.sanitizeDashboardsForInlineActions() } func (cfg *Config) sanitizeDashboardsForInlineActions() { for _, dashboard := range cfg.Dashboards { cfg.sanitizeDashboardComponentForInlineActions(dashboard) } } func (cfg *Config) sanitizeDashboardComponentForInlineActions(component *DashboardComponent) { visited := make(map[*DashboardComponent]bool) cfg.sanitizeDashboardComponentForInlineActionsHelper(component, visited) } func (cfg *Config) sanitizeDashboardComponentForInlineActionsHelper(component *DashboardComponent, visited map[*DashboardComponent]bool) { if component == nil { return } if visited[component] { return } visited[component] = true cfg.sanitizeInlineAction(component) cfg.sanitizeChildDashboardComponents(component, visited) } func (cfg *Config) sanitizeInlineAction(component *DashboardComponent) { if component.InlineAction == nil { return } sanitizeInlineActionTitles(component) if component.Entity != "" && component.InlineAction.Entity == "" { component.InlineAction.Entity = component.Entity } component.InlineAction.sanitize(cfg) cfg.addInlineActionIfNotExists(component.InlineAction) } func (cfg *Config) addInlineActionIfNotExists(action *Action) { if cfg.inlineActionExists(action) { return } cfg.Actions = append(cfg.Actions, action) } func sanitizeInlineActionTitles(component *DashboardComponent) { if component.InlineAction.Title == "" { component.InlineAction.Title = component.Title } if component.Title == "" { component.Title = component.InlineAction.Title } } func (cfg *Config) inlineActionExists(action *Action) bool { if cfg.inlineActionPointerExists(action) { return true } if cfg.inlineActionIDExists(action) { return true } return false } func (cfg *Config) inlineActionPointerExists(action *Action) bool { for _, existingAction := range cfg.Actions { if existingAction == action { return true } } return false } func (cfg *Config) inlineActionIDExists(action *Action) bool { if action.ID == "" { return false } for _, existingAction := range cfg.Actions { if existingAction.ID == action.ID { return true } } return false } func (cfg *Config) sanitizeChildDashboardComponents(component *DashboardComponent, visited map[*DashboardComponent]bool) { for _, child := range component.Contents { if child.Entity == "" { child.Entity = component.Entity } cfg.sanitizeDashboardComponentForInlineActionsHelper(child, visited) } } func (cfg *Config) sanitizeLogLevel() { if logLevel, err := log.ParseLevel(cfg.LogLevel); err == nil { log.Info("Setting log level to ", logLevel) log.SetLevel(logLevel) } } func (action *Action) sanitize(cfg *Config) { if action.Timeout < 3 { action.Timeout = 3 } action.ID = getActionID(action) action.Icon = lookupHTMLIcon(action.Icon, cfg.DefaultIconForActions) action.PopupOnStart = sanitizePopupOnStart(action.PopupOnStart, cfg) sanitizeActionExecutionMode(action) if action.MaxConcurrent < 1 { action.MaxConcurrent = 1 } for idx := range action.Arguments { action.Arguments[idx].sanitize() } } func sanitizeActionExecutionMode(action *Action) { prioritizeExecToolOverShellAndExec(action) if len(action.Exec) > 0 && action.Shell != "" { log.Warnf("Action %q has both shell and exec; using exec only", action.Title) action.Shell = "" } clearIncompleteExecTool(action) } func prioritizeExecToolOverShellAndExec(action *Action) { if !execToolIsFullyConfigured(action) { return } if action.Shell == "" && len(action.Exec) == 0 { return } log.Warnf("Action %q has both execTool and shell/exec; using execTool only", action.Title) action.Shell = "" action.Exec = nil } func execToolIsFullyConfigured(action *Action) bool { t := action.ExecTool if t == nil { return false } if t.Name == "" { return false } return t.Config != nil } func clearIncompleteExecTool(action *Action) { if action.ExecTool == nil || (action.ExecTool.Name != "" && action.ExecTool.Config != nil) { return } log.Warnf("Action %q has execTool with missing name or config; clearing execTool", action.Title) action.ExecTool = nil } func (cfg *Config) sanitizeAuthRequireGuestsToLogin() { if cfg.AuthRequireGuestsToLogin { log.Infof("AuthRequireGuestsToLogin is enabled. All defaultPermissions will be set to false") cfg.DefaultPermissions.View = false cfg.DefaultPermissions.Exec = false cfg.DefaultPermissions.Logs = false } } func (cfg *Config) sanitizeLogHistoryPageSize() { if cfg.LogHistoryPageSize < 10 { log.Warnf("LogsHistoryLimit is too low, setting it to 10") cfg.LogHistoryPageSize = 10 } else if cfg.LogHistoryPageSize > 100 { log.Warnf("LogsHistoryLimit is high, you can do this, but expect browser lag.") } } func (cfg *Config) sanitizeLocalUserPasswords() { for _, user := range cfg.AuthLocalUsers.Users { if user.Password != "" { user.Password = parsePasswordTemplate(user.Password) } } } // parsePasswordTemplate expands {{ .Env.VAR }} in local user password fields using the process environment. func parsePasswordTemplate(source string) string { t, err := template.New("password").Option("missingkey=error").Parse(source) if err != nil { log.WithFields(log.Fields{"error": err}).Debug("Password template parse failed, using literal") return source } var b strings.Builder if err := t.Execute(&b, map[string]interface{}{"Env": env.BuildEnvMap()}); err != nil { log.WithFields(log.Fields{"error": err}).Debug("Password template execute failed, using literal") return source } return b.String() } func getActionID(action *Action) string { if action.ID == "" { return uuid.NewString() } if strings.Contains(action.ID, "{{") { log.Fatalf("Action IDs cannot contain variables") } return action.ID } //gocyclo:ignore func sanitizePopupOnStart(raw string, cfg *Config) string { switch raw { case "execution-dialog": return raw case "execution-dialog-output-html": return raw case "execution-dialog-stdout-only": return raw case "execution-button": return raw default: return cfg.DefaultPopupOnStart } } func (arg *ActionArgument) sanitize() { if arg.Title == "" { arg.Title = arg.Name } for idx, choice := range arg.Choices { if choice.Title == "" { arg.Choices[idx].Title = choice.Value } } arg.sanitizeNoType() // TODO Validate the default against the type checker, but this creates a // import loop } func (arg *ActionArgument) sanitizeNoType() { if len(arg.Choices) == 0 && arg.Type == "" { log.WithFields(log.Fields{ "arg": arg.Name, }).Warn("Argument type isn't set, will default to 'ascii' but this may not be safe. You should set a type specifically.") arg.Type = "ascii" } }