package config import ( "fmt" "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.sanitizeLocalUsers() cfg.sanitizeSecurityHeaders() cfg.sanitizeOnClickDefaults() // log.Infof("cfg %p", cfg) for idx := range cfg.Actions { cfg.Actions[idx].sanitize(cfg) } cfg.sanitizeDashboardsForInlineActions() cfg.sanitizeActionGroups() cfg.sanitizeActionGroupReferences() if err := cfg.validateReservedActionArgumentNames(); err != nil { log.Fatalf("%v", err) } } func (cfg *Config) validateReservedActionArgumentNames() error { for _, action := range cfg.Actions { if err := action.validateReservedArgumentNames(); err != nil { return err } } return nil } func (action *Action) validateReservedArgumentNames() error { if action == nil { return nil } for _, arg := range action.Arguments { if strings.HasPrefix(arg.Name, ReservedArgumentNamePrefix) { return fmt.Errorf("action %q argument %q uses reserved prefix %q", action.Title, arg.Name, ReservedArgumentNamePrefix) } } return nil } 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) migrateActionOnClick(action) action.OnClick = sanitizeOnClick(action.OnClick, cfg) action.PopupOnStart = action.OnClick if action.MaxConcurrent < 1 { action.MaxConcurrent = 1 } action.Groups = dedupeStrings(action.Groups) for idx := range action.Arguments { action.Arguments[idx].sanitize() } } func dedupeStrings(values []string) []string { seen := make(map[string]struct{}, len(values)) out := make([]string, 0, len(values)) for _, value := range values { out = appendUniqueString(out, seen, value) } return out } func appendUniqueString(out []string, seen map[string]struct{}, value string) []string { if value == "" { return out } if _, found := seen[value]; found { return out } seen[value] = struct{}{} return append(out, value) } const defaultActionGroupQueueSize = 5 func (cfg *Config) sanitizeActionGroups() { for _, group := range cfg.ActionGroups { if group == nil { continue } if group.QueueSize <= 0 { group.QueueSize = defaultActionGroupQueueSize } group.Icon = lookupHTMLIcon(group.Icon, cfg.DefaultIconForActions) } } func (cfg *Config) sanitizeActionGroupReferences() { for _, action := range cfg.Actions { for _, groupName := range action.Groups { cfg.warnInvalidActionGroupReference(action, groupName) } } } func (cfg *Config) warnInvalidActionGroupReference(action *Action, groupName string) { group, found := cfg.ActionGroups[groupName] if !found { log.WithFields(log.Fields{ "actionTitle": action.Title, "groupName": groupName, }).Warn("Action references unknown action group") return } if group == nil || group.MaxConcurrent < 1 { log.WithFields(log.Fields{ "actionTitle": action.Title, "groupName": groupName, }).Warn("Action references action group that will not be enforced at runtime") } } 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 cfg.DefaultPermissions.Kill = 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) sanitizeLocalUsers() { for _, user := range cfg.AuthLocalUsers.Users { expandLocalUserEnvTemplates(user) } if err := validateUniqueLocalUserAPIKeys(cfg.AuthLocalUsers.Users); err != nil { log.Fatalf("%v", err) } } func expandLocalUserEnvTemplates(user *LocalUser) { if user == nil { return } if user.Password != "" { user.Password = expandEnvTemplate(user.Password) } if user.ApiKey != "" { user.ApiKey = expandEnvTemplate(user.ApiKey) } } // validateUniqueLocalUserAPIKeys returns an error when two local users share the same non-empty apiKey. func validateUniqueLocalUserAPIKeys(users []*LocalUser) error { seen := make(map[string]string) for _, user := range users { if err := recordUniqueLocalUserAPIKey(seen, user); err != nil { return err } } return nil } func recordUniqueLocalUserAPIKey(seen map[string]string, user *LocalUser) error { if user == nil || user.ApiKey == "" { return nil } if prior, ok := seen[user.ApiKey]; ok { return fmt.Errorf("duplicate authLocalUsers apiKey for users %q and %q", prior, user.Username) } seen[user.ApiKey] = user.Username return nil } func (cfg *Config) sanitizeSecurityHeaders() { cfg.sanitizeSecurityHeadersCSP() cfg.sanitizeSecurityHeadersXFrameOptions() } func (cfg *Config) sanitizeSecurityHeadersCSP() { if !cfg.Security.HeaderContentSecurityPolicy || cfg.Security.ContentSecurityPolicy != "" { return } cfg.Security.ContentSecurityPolicy = ContentSecurityPolicyDefault } func (cfg *Config) sanitizeSecurityHeadersXFrameOptions() { if !cfg.Security.HeaderXFrameOptions || cfg.Security.XFrameOptions != "" { return } cfg.Security.XFrameOptions = "DENY" } // expandEnvTemplate expands {{ .Env.VAR }} in config strings using the process environment. func expandEnvTemplate(source string) string { t, err := template.New("envTemplate").Option("missingkey=error").Parse(source) if err != nil { log.WithFields(log.Fields{"error": err}).Debug("Env 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("Env 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 sanitizeOnClick(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 case "history": return raw default: return cfg.DefaultOnClick } } func migrateActionOnClick(action *Action) { if action.OnClick == "" && action.PopupOnStart != "" { action.OnClick = action.PopupOnStart } } func shouldMigrateDefaultOnClickFromPopup(onClick, popupOnStart string) bool { if popupOnStart == "" { return false } if onClick == "" { return true } return onClick == "nothing" && popupOnStart != "nothing" } func (cfg *Config) migrateDefaultOnClickFromLegacyPopup() { if !shouldMigrateDefaultOnClickFromPopup(cfg.DefaultOnClick, cfg.DefaultPopupOnStart) { return } cfg.DefaultOnClick = cfg.DefaultPopupOnStart } func (cfg *Config) sanitizeOnClickDefaults() { cfg.migrateDefaultOnClickFromLegacyPopup() if cfg.DefaultOnClick == "" { cfg.DefaultOnClick = "nothing" } cfg.DefaultPopupOnStart = cfg.DefaultOnClick } 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() // Default value validation runs in executor at config load (validateArgumentDefaults). } 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" } }